decision_agent 0.1.7 → 0.3.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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +41 -1
  3. data/bin/decision_agent +104 -0
  4. data/lib/decision_agent/dmn/adapter.rb +135 -0
  5. data/lib/decision_agent/dmn/cache.rb +306 -0
  6. data/lib/decision_agent/dmn/decision_graph.rb +327 -0
  7. data/lib/decision_agent/dmn/decision_tree.rb +192 -0
  8. data/lib/decision_agent/dmn/errors.rb +30 -0
  9. data/lib/decision_agent/dmn/exporter.rb +217 -0
  10. data/lib/decision_agent/dmn/feel/evaluator.rb +797 -0
  11. data/lib/decision_agent/dmn/feel/functions.rb +420 -0
  12. data/lib/decision_agent/dmn/feel/parser.rb +349 -0
  13. data/lib/decision_agent/dmn/feel/simple_parser.rb +276 -0
  14. data/lib/decision_agent/dmn/feel/transformer.rb +372 -0
  15. data/lib/decision_agent/dmn/feel/types.rb +276 -0
  16. data/lib/decision_agent/dmn/importer.rb +77 -0
  17. data/lib/decision_agent/dmn/model.rb +197 -0
  18. data/lib/decision_agent/dmn/parser.rb +191 -0
  19. data/lib/decision_agent/dmn/testing.rb +333 -0
  20. data/lib/decision_agent/dmn/validator.rb +315 -0
  21. data/lib/decision_agent/dmn/versioning.rb +229 -0
  22. data/lib/decision_agent/dmn/visualizer.rb +513 -0
  23. data/lib/decision_agent/dsl/condition_evaluator.rb +1132 -12
  24. data/lib/decision_agent/dsl/schema_validator.rb +12 -1
  25. data/lib/decision_agent/evaluators/dmn_evaluator.rb +221 -0
  26. data/lib/decision_agent/version.rb +1 -1
  27. data/lib/decision_agent/web/dmn_editor.rb +426 -0
  28. data/lib/decision_agent/web/public/app.js +119 -1
  29. data/lib/decision_agent/web/public/dmn-editor.css +596 -0
  30. data/lib/decision_agent/web/public/dmn-editor.html +250 -0
  31. data/lib/decision_agent/web/public/dmn-editor.js +553 -0
  32. data/lib/decision_agent/web/public/index.html +71 -0
  33. data/lib/decision_agent/web/public/styles.css +21 -0
  34. data/lib/decision_agent/web/server.rb +465 -0
  35. data/spec/ab_testing/ab_testing_agent_spec.rb +174 -0
  36. data/spec/advanced_operators_spec.rb +2147 -0
  37. data/spec/auth/rbac_adapter_spec.rb +228 -0
  38. data/spec/dmn/decision_graph_spec.rb +282 -0
  39. data/spec/dmn/decision_tree_spec.rb +203 -0
  40. data/spec/dmn/feel/errors_spec.rb +18 -0
  41. data/spec/dmn/feel/functions_spec.rb +400 -0
  42. data/spec/dmn/feel/simple_parser_spec.rb +274 -0
  43. data/spec/dmn/feel/types_spec.rb +176 -0
  44. data/spec/dmn/feel_parser_spec.rb +489 -0
  45. data/spec/dmn/hit_policy_spec.rb +202 -0
  46. data/spec/dmn/integration_spec.rb +226 -0
  47. data/spec/examples.txt +1909 -0
  48. data/spec/fixtures/dmn/complex_decision.dmn +81 -0
  49. data/spec/fixtures/dmn/invalid_structure.dmn +31 -0
  50. data/spec/fixtures/dmn/simple_decision.dmn +40 -0
  51. data/spec/monitoring/metrics_collector_spec.rb +37 -35
  52. data/spec/monitoring/monitored_agent_spec.rb +14 -11
  53. data/spec/performance_optimizations_spec.rb +10 -3
  54. data/spec/thread_safety_spec.rb +10 -2
  55. data/spec/web_ui_rack_spec.rb +294 -0
  56. metadata +66 -1
@@ -1439,6 +1439,300 @@ RSpec.describe "DecisionAgent Web UI Rack Integration" do
1439
1439
  end
1440
1440
  end
1441
1441
 
1442
+ describe "Versioning API integration tests with real FileStorageAdapter" do
1443
+ let(:temp_storage_path) { Dir.mktmpdir("versioning_test_") }
1444
+ let(:authenticator) { DecisionAgent::Web::Server.authenticator }
1445
+ let(:user) do
1446
+ u = authenticator.create_user(
1447
+ email: "versioninteg@example.com",
1448
+ password: "password123",
1449
+ roles: [:editor]
1450
+ )
1451
+ session = authenticator.login("versioninteg@example.com", "password123")
1452
+ { user: u, session: session }
1453
+ end
1454
+
1455
+ before do
1456
+ # Create a real FileStorageAdapter and inject it into the server's version_manager
1457
+ real_adapter = DecisionAgent::Versioning::FileStorageAdapter.new(storage_path: temp_storage_path)
1458
+ real_version_manager = DecisionAgent::Versioning::VersionManager.new(adapter: real_adapter)
1459
+
1460
+ # Stub the version_manager method to return our real manager
1461
+ allow_any_instance_of(DecisionAgent::Web::Server).to receive(:version_manager).and_return(real_version_manager)
1462
+ end
1463
+
1464
+ after do
1465
+ # Clean up temp directory
1466
+ FileUtils.rm_rf(temp_storage_path)
1467
+ end
1468
+
1469
+ describe "POST /api/versions" do
1470
+ it "creates a version with real file storage" do
1471
+ rule_content = {
1472
+ version: "1.0",
1473
+ ruleset: "test_rules",
1474
+ rules: [{
1475
+ id: "rule1",
1476
+ if: { field: "amount", op: "gt", value: 100 },
1477
+ then: { decision: "approve", weight: 0.9, reason: "High amount" }
1478
+ }]
1479
+ }
1480
+
1481
+ post "/api/versions",
1482
+ {
1483
+ rule_id: "integration_test_rule",
1484
+ content: rule_content,
1485
+ created_by: "integration@example.com",
1486
+ changelog: "Integration test version"
1487
+ }.to_json,
1488
+ { "CONTENT_TYPE" => "application/json", "HTTP_AUTHORIZATION" => "Bearer #{user[:session].token}" }
1489
+
1490
+ expect(last_response.status).to eq(201)
1491
+ json = JSON.parse(last_response.body)
1492
+ expect(json["rule_id"]).to eq("integration_test_rule")
1493
+ expect(json["version_number"]).to eq(1)
1494
+ expect(json["status"]).to eq("active") # FileStorageAdapter defaults to "active"
1495
+ expect(json["created_by"]).to eq("integration@example.com")
1496
+
1497
+ # Verify file was actually created
1498
+ rule_dir = File.join(temp_storage_path, "integration_test_rule")
1499
+ expect(Dir.exist?(rule_dir)).to be true
1500
+ version_file = File.join(rule_dir, "1.json")
1501
+ expect(File.exist?(version_file)).to be true
1502
+
1503
+ # Verify content
1504
+ stored_content = JSON.parse(File.read(version_file))
1505
+ # JSON parsing returns string keys, so we compare by converting both to same format
1506
+ expected_content = JSON.parse(JSON.generate(rule_content))
1507
+ expect(stored_content["content"]).to eq(expected_content)
1508
+ end
1509
+
1510
+ it "creates multiple versions and increments version number" do
1511
+ rule_content = { version: "1.0", ruleset: "test", rules: [] }
1512
+
1513
+ # Create first version
1514
+ post "/api/versions",
1515
+ {
1516
+ rule_id: "multi_version_rule",
1517
+ content: rule_content,
1518
+ created_by: "test@example.com"
1519
+ }.to_json,
1520
+ { "CONTENT_TYPE" => "application/json", "HTTP_AUTHORIZATION" => "Bearer #{user[:session].token}" }
1521
+
1522
+ expect(last_response.status).to eq(201)
1523
+ json1 = JSON.parse(last_response.body)
1524
+ expect(json1["version_number"]).to eq(1)
1525
+
1526
+ # Create second version
1527
+ post "/api/versions",
1528
+ {
1529
+ rule_id: "multi_version_rule",
1530
+ content: rule_content.merge(version: "2.0"),
1531
+ created_by: "test@example.com"
1532
+ }.to_json,
1533
+ { "CONTENT_TYPE" => "application/json", "HTTP_AUTHORIZATION" => "Bearer #{user[:session].token}" }
1534
+
1535
+ expect(last_response.status).to eq(201)
1536
+ json2 = JSON.parse(last_response.body)
1537
+ expect(json2["version_number"]).to eq(2)
1538
+ end
1539
+ end
1540
+
1541
+ describe "GET /api/rules/:rule_id/versions" do
1542
+ it "returns versions from real file storage" do
1543
+ rule_content = { version: "1.0", ruleset: "test", rules: [] }
1544
+
1545
+ # Create two versions
1546
+ post "/api/versions",
1547
+ {
1548
+ rule_id: "list_test_rule",
1549
+ content: rule_content,
1550
+ created_by: "test@example.com"
1551
+ }.to_json,
1552
+ { "CONTENT_TYPE" => "application/json", "HTTP_AUTHORIZATION" => "Bearer #{user[:session].token}" }
1553
+
1554
+ post "/api/versions",
1555
+ {
1556
+ rule_id: "list_test_rule",
1557
+ content: rule_content,
1558
+ created_by: "test@example.com"
1559
+ }.to_json,
1560
+ { "CONTENT_TYPE" => "application/json", "HTTP_AUTHORIZATION" => "Bearer #{user[:session].token}" }
1561
+
1562
+ # List versions
1563
+ get "/api/rules/list_test_rule/versions", {}, { "HTTP_AUTHORIZATION" => "Bearer #{user[:session].token}" }
1564
+
1565
+ expect(last_response.status).to eq(200)
1566
+ json = JSON.parse(last_response.body)
1567
+ expect(json).to be_an(Array)
1568
+ expect(json.length).to eq(2)
1569
+ expect(json.first["version_number"]).to eq(2) # Most recent first
1570
+ expect(json.last["version_number"]).to eq(1)
1571
+ end
1572
+
1573
+ it "respects limit parameter" do
1574
+ rule_content = { version: "1.0", ruleset: "test", rules: [] }
1575
+
1576
+ # Create three versions
1577
+ 3.times do
1578
+ post "/api/versions",
1579
+ {
1580
+ rule_id: "limit_test_rule",
1581
+ content: rule_content,
1582
+ created_by: "test@example.com"
1583
+ }.to_json,
1584
+ { "CONTENT_TYPE" => "application/json", "HTTP_AUTHORIZATION" => "Bearer #{user[:session].token}" }
1585
+ end
1586
+
1587
+ # List with limit
1588
+ get "/api/rules/limit_test_rule/versions?limit=2", {}, { "HTTP_AUTHORIZATION" => "Bearer #{user[:session].token}" }
1589
+
1590
+ expect(last_response.status).to eq(200)
1591
+ json = JSON.parse(last_response.body)
1592
+ expect(json.length).to eq(2)
1593
+ end
1594
+ end
1595
+
1596
+ describe "GET /api/rules/:rule_id/history" do
1597
+ it "returns history from real file storage" do
1598
+ rule_content = { version: "1.0", ruleset: "test", rules: [] }
1599
+
1600
+ # Create a version
1601
+ post "/api/versions",
1602
+ {
1603
+ rule_id: "history_test_rule",
1604
+ content: rule_content,
1605
+ created_by: "test@example.com",
1606
+ changelog: "Test changelog"
1607
+ }.to_json,
1608
+ { "CONTENT_TYPE" => "application/json", "HTTP_AUTHORIZATION" => "Bearer #{user[:session].token}" }
1609
+
1610
+ # Get history
1611
+ get "/api/rules/history_test_rule/history", {}, { "HTTP_AUTHORIZATION" => "Bearer #{user[:session].token}" }
1612
+
1613
+ expect(last_response.status).to eq(200)
1614
+ json = JSON.parse(last_response.body)
1615
+ expect(json).to be_a(Hash)
1616
+ expect(json["total_versions"]).to eq(1)
1617
+ expect(json["versions"]).to be_an(Array)
1618
+ expect(json["versions"].length).to eq(1)
1619
+ end
1620
+ end
1621
+
1622
+ describe "GET /api/versions/:version_id" do
1623
+ it "retrieves a specific version from real file storage" do
1624
+ rule_content = {
1625
+ version: "1.0",
1626
+ ruleset: "test",
1627
+ rules: [{ id: "test_rule", if: { field: "x", op: "eq", value: 1 }, then: { decision: "yes" } }]
1628
+ }
1629
+
1630
+ # Create a version
1631
+ post "/api/versions",
1632
+ {
1633
+ rule_id: "get_version_test",
1634
+ content: rule_content,
1635
+ created_by: "test@example.com"
1636
+ }.to_json,
1637
+ { "CONTENT_TYPE" => "application/json", "HTTP_AUTHORIZATION" => "Bearer #{user[:session].token}" }
1638
+
1639
+ version_json = JSON.parse(last_response.body)
1640
+ version_id = version_json["id"]
1641
+
1642
+ # Get the version
1643
+ get "/api/versions/#{version_id}", {}, { "HTTP_AUTHORIZATION" => "Bearer #{user[:session].token}" }
1644
+
1645
+ expect(last_response.status).to eq(200)
1646
+ json = JSON.parse(last_response.body)
1647
+ expect(json["id"]).to eq(version_id)
1648
+ expect(json["rule_id"]).to eq("get_version_test")
1649
+ # JSON parsing returns string keys, so we compare by converting both to same format
1650
+ expected_content = JSON.parse(JSON.generate(rule_content))
1651
+ expect(json["content"]).to eq(expected_content)
1652
+ end
1653
+ end
1654
+
1655
+ describe "POST /api/versions/:version_id/activate" do
1656
+ it "activates a version with real file storage" do
1657
+ # Create admin user for deploy permission
1658
+ authenticator.create_user(
1659
+ email: "deployadmin@example.com",
1660
+ password: "password123",
1661
+ roles: [:admin]
1662
+ )
1663
+ admin_session = authenticator.login("deployadmin@example.com", "password123")
1664
+
1665
+ rule_content = { version: "1.0", ruleset: "test", rules: [] }
1666
+
1667
+ # Create a version
1668
+ post "/api/versions",
1669
+ {
1670
+ rule_id: "activate_test_rule",
1671
+ content: rule_content,
1672
+ created_by: "test@example.com"
1673
+ }.to_json,
1674
+ { "CONTENT_TYPE" => "application/json", "HTTP_AUTHORIZATION" => "Bearer #{user[:session].token}" }
1675
+
1676
+ version_json = JSON.parse(last_response.body)
1677
+ version_id = version_json["id"]
1678
+
1679
+ # Activate the version
1680
+ post "/api/versions/#{version_id}/activate",
1681
+ { performed_by: "admin@example.com" }.to_json,
1682
+ { "CONTENT_TYPE" => "application/json", "HTTP_AUTHORIZATION" => "Bearer #{admin_session.token}" }
1683
+
1684
+ expect(last_response.status).to eq(200)
1685
+ json = JSON.parse(last_response.body)
1686
+ expect(json["id"]).to eq(version_id)
1687
+ expect(json["status"]).to eq("active")
1688
+
1689
+ # Verify it's active by getting the version again
1690
+ get "/api/versions/#{version_id}", {}, { "HTTP_AUTHORIZATION" => "Bearer #{user[:session].token}" }
1691
+ active_json = JSON.parse(last_response.body)
1692
+ expect(active_json["status"]).to eq("active")
1693
+ end
1694
+ end
1695
+
1696
+ describe "GET /api/versions/:version_id_1/compare/:version_id_2" do
1697
+ it "compares two versions from real file storage" do
1698
+ base_content = { version: "1.0", ruleset: "test", rules: [{ id: "r1" }] }
1699
+ modified_content = { version: "1.0", ruleset: "test", rules: [{ id: "r1" }, { id: "r2" }] }
1700
+
1701
+ # Create first version
1702
+ post "/api/versions",
1703
+ {
1704
+ rule_id: "compare_test_rule",
1705
+ content: base_content,
1706
+ created_by: "test@example.com"
1707
+ }.to_json,
1708
+ { "CONTENT_TYPE" => "application/json", "HTTP_AUTHORIZATION" => "Bearer #{user[:session].token}" }
1709
+ v1_json = JSON.parse(last_response.body)
1710
+ v1_id = v1_json["id"]
1711
+
1712
+ # Create second version
1713
+ post "/api/versions",
1714
+ {
1715
+ rule_id: "compare_test_rule",
1716
+ content: modified_content,
1717
+ created_by: "test@example.com"
1718
+ }.to_json,
1719
+ { "CONTENT_TYPE" => "application/json", "HTTP_AUTHORIZATION" => "Bearer #{user[:session].token}" }
1720
+ v2_json = JSON.parse(last_response.body)
1721
+ v2_id = v2_json["id"]
1722
+
1723
+ # Compare versions
1724
+ get "/api/versions/#{v1_id}/compare/#{v2_id}", {}, { "HTTP_AUTHORIZATION" => "Bearer #{user[:session].token}" }
1725
+
1726
+ expect(last_response.status).to eq(200)
1727
+ json = JSON.parse(last_response.body)
1728
+ expect(json).to be_a(Hash)
1729
+ expect(json).to have_key("version_1") # compare_versions returns version_1 and version_2
1730
+ expect(json).to have_key("version_2")
1731
+ expect(json).to have_key("differences")
1732
+ end
1733
+ end
1734
+ end
1735
+
1442
1736
  describe "Batch Testing API comprehensive tests" do
1443
1737
  describe "POST /api/testing/batch/import" do
1444
1738
  it "handles Excel file import" do
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: decision_agent
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.7
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sam Aswin
@@ -37,6 +37,34 @@ dependencies:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
39
  version: '1.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: nokogiri
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '1.15'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '1.15'
54
+ - !ruby/object:Gem::Dependency
55
+ name: parslet
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '2.0'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '2.0'
40
68
  - !ruby/object:Gem::Dependency
41
69
  name: roo
42
70
  requirement: !ruby/object:Gem::Requirement
@@ -174,6 +202,25 @@ files:
174
202
  - lib/decision_agent/auth/user.rb
175
203
  - lib/decision_agent/context.rb
176
204
  - lib/decision_agent/decision.rb
205
+ - lib/decision_agent/dmn/adapter.rb
206
+ - lib/decision_agent/dmn/cache.rb
207
+ - lib/decision_agent/dmn/decision_graph.rb
208
+ - lib/decision_agent/dmn/decision_tree.rb
209
+ - lib/decision_agent/dmn/errors.rb
210
+ - lib/decision_agent/dmn/exporter.rb
211
+ - lib/decision_agent/dmn/feel/evaluator.rb
212
+ - lib/decision_agent/dmn/feel/functions.rb
213
+ - lib/decision_agent/dmn/feel/parser.rb
214
+ - lib/decision_agent/dmn/feel/simple_parser.rb
215
+ - lib/decision_agent/dmn/feel/transformer.rb
216
+ - lib/decision_agent/dmn/feel/types.rb
217
+ - lib/decision_agent/dmn/importer.rb
218
+ - lib/decision_agent/dmn/model.rb
219
+ - lib/decision_agent/dmn/parser.rb
220
+ - lib/decision_agent/dmn/testing.rb
221
+ - lib/decision_agent/dmn/validator.rb
222
+ - lib/decision_agent/dmn/versioning.rb
223
+ - lib/decision_agent/dmn/visualizer.rb
177
224
  - lib/decision_agent/dsl/condition_evaluator.rb
178
225
  - lib/decision_agent/dsl/rule_parser.rb
179
226
  - lib/decision_agent/dsl/schema_validator.rb
@@ -181,6 +228,7 @@ files:
181
228
  - lib/decision_agent/evaluation.rb
182
229
  - lib/decision_agent/evaluation_validator.rb
183
230
  - lib/decision_agent/evaluators/base.rb
231
+ - lib/decision_agent/evaluators/dmn_evaluator.rb
184
232
  - lib/decision_agent/evaluators/json_rule_evaluator.rb
185
233
  - lib/decision_agent/evaluators/static_evaluator.rb
186
234
  - lib/decision_agent/monitoring/alert_manager.rb
@@ -210,10 +258,14 @@ files:
210
258
  - lib/decision_agent/versioning/adapter.rb
211
259
  - lib/decision_agent/versioning/file_storage_adapter.rb
212
260
  - lib/decision_agent/versioning/version_manager.rb
261
+ - lib/decision_agent/web/dmn_editor.rb
213
262
  - lib/decision_agent/web/middleware/auth_middleware.rb
214
263
  - lib/decision_agent/web/middleware/permission_middleware.rb
215
264
  - lib/decision_agent/web/public/app.js
216
265
  - lib/decision_agent/web/public/batch_testing.html
266
+ - lib/decision_agent/web/public/dmn-editor.css
267
+ - lib/decision_agent/web/public/dmn-editor.html
268
+ - lib/decision_agent/web/public/dmn-editor.js
217
269
  - lib/decision_agent/web/public/index.html
218
270
  - lib/decision_agent/web/public/login.html
219
271
  - lib/decision_agent/web/public/styles.css
@@ -260,12 +312,25 @@ files:
260
312
  - spec/context_spec.rb
261
313
  - spec/decision_agent_spec.rb
262
314
  - spec/decision_spec.rb
315
+ - spec/dmn/decision_graph_spec.rb
316
+ - spec/dmn/decision_tree_spec.rb
317
+ - spec/dmn/feel/errors_spec.rb
318
+ - spec/dmn/feel/functions_spec.rb
319
+ - spec/dmn/feel/simple_parser_spec.rb
320
+ - spec/dmn/feel/types_spec.rb
321
+ - spec/dmn/feel_parser_spec.rb
322
+ - spec/dmn/hit_policy_spec.rb
323
+ - spec/dmn/integration_spec.rb
263
324
  - spec/dsl/condition_evaluator_spec.rb
264
325
  - spec/dsl_validation_spec.rb
265
326
  - spec/edge_cases_spec.rb
266
327
  - spec/evaluation_spec.rb
267
328
  - spec/evaluation_validator_spec.rb
329
+ - spec/examples.txt
268
330
  - spec/examples/feedback_aware_evaluator_spec.rb
331
+ - spec/fixtures/dmn/complex_decision.dmn
332
+ - spec/fixtures/dmn/invalid_structure.dmn
333
+ - spec/fixtures/dmn/simple_decision.dmn
269
334
  - spec/issue_verification_spec.rb
270
335
  - spec/json_rule_evaluator_spec.rb
271
336
  - spec/monitoring/alert_manager_spec.rb