decision_agent 0.2.0 → 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.
- checksums.yaml +4 -4
- data/README.md +41 -1
- data/bin/decision_agent +104 -0
- data/lib/decision_agent/dmn/adapter.rb +135 -0
- data/lib/decision_agent/dmn/cache.rb +306 -0
- data/lib/decision_agent/dmn/decision_graph.rb +327 -0
- data/lib/decision_agent/dmn/decision_tree.rb +192 -0
- data/lib/decision_agent/dmn/errors.rb +30 -0
- data/lib/decision_agent/dmn/exporter.rb +217 -0
- data/lib/decision_agent/dmn/feel/evaluator.rb +797 -0
- data/lib/decision_agent/dmn/feel/functions.rb +420 -0
- data/lib/decision_agent/dmn/feel/parser.rb +349 -0
- data/lib/decision_agent/dmn/feel/simple_parser.rb +276 -0
- data/lib/decision_agent/dmn/feel/transformer.rb +372 -0
- data/lib/decision_agent/dmn/feel/types.rb +276 -0
- data/lib/decision_agent/dmn/importer.rb +77 -0
- data/lib/decision_agent/dmn/model.rb +197 -0
- data/lib/decision_agent/dmn/parser.rb +191 -0
- data/lib/decision_agent/dmn/testing.rb +333 -0
- data/lib/decision_agent/dmn/validator.rb +315 -0
- data/lib/decision_agent/dmn/versioning.rb +229 -0
- data/lib/decision_agent/dmn/visualizer.rb +513 -0
- data/lib/decision_agent/dsl/condition_evaluator.rb +3 -0
- data/lib/decision_agent/dsl/schema_validator.rb +2 -1
- data/lib/decision_agent/evaluators/dmn_evaluator.rb +221 -0
- data/lib/decision_agent/version.rb +1 -1
- data/lib/decision_agent/web/dmn_editor.rb +426 -0
- data/lib/decision_agent/web/public/dmn-editor.css +596 -0
- data/lib/decision_agent/web/public/dmn-editor.html +250 -0
- data/lib/decision_agent/web/public/dmn-editor.js +553 -0
- data/lib/decision_agent/web/public/index.html +3 -0
- data/lib/decision_agent/web/public/styles.css +21 -0
- data/lib/decision_agent/web/server.rb +465 -0
- data/spec/ab_testing/ab_testing_agent_spec.rb +174 -0
- data/spec/auth/rbac_adapter_spec.rb +228 -0
- data/spec/dmn/decision_graph_spec.rb +282 -0
- data/spec/dmn/decision_tree_spec.rb +203 -0
- data/spec/dmn/feel/errors_spec.rb +18 -0
- data/spec/dmn/feel/functions_spec.rb +400 -0
- data/spec/dmn/feel/simple_parser_spec.rb +274 -0
- data/spec/dmn/feel/types_spec.rb +176 -0
- data/spec/dmn/feel_parser_spec.rb +489 -0
- data/spec/dmn/hit_policy_spec.rb +202 -0
- data/spec/dmn/integration_spec.rb +226 -0
- data/spec/examples.txt +1846 -1570
- data/spec/fixtures/dmn/complex_decision.dmn +81 -0
- data/spec/fixtures/dmn/invalid_structure.dmn +31 -0
- data/spec/fixtures/dmn/simple_decision.dmn +40 -0
- data/spec/monitoring/metrics_collector_spec.rb +37 -35
- data/spec/monitoring/monitored_agent_spec.rb +14 -11
- data/spec/performance_optimizations_spec.rb +10 -3
- data/spec/thread_safety_spec.rb +10 -2
- data/spec/web_ui_rack_spec.rb +294 -0
- metadata +65 -1
|
@@ -547,4 +547,232 @@ RSpec.describe DecisionAgent::Auth::RbacAdapter do
|
|
|
547
547
|
end
|
|
548
548
|
end
|
|
549
549
|
end
|
|
550
|
+
|
|
551
|
+
describe "Integration tests with real User and Role objects" do
|
|
552
|
+
let(:adapter) { DecisionAgent::Auth::DefaultAdapter.new }
|
|
553
|
+
|
|
554
|
+
describe "with real User and Role objects" do
|
|
555
|
+
it "checks permissions using real Role class" do
|
|
556
|
+
user = DecisionAgent::Auth::User.new(
|
|
557
|
+
id: "user1",
|
|
558
|
+
email: "admin@example.com",
|
|
559
|
+
password: "password123",
|
|
560
|
+
roles: [:admin]
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
expect(adapter.can?(user, :read)).to be true
|
|
564
|
+
expect(adapter.can?(user, :write)).to be true
|
|
565
|
+
expect(adapter.can?(user, :delete)).to be true
|
|
566
|
+
expect(adapter.can?(user, :manage_users)).to be true
|
|
567
|
+
expect(adapter.can?(user, :audit)).to be true
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
it "checks permissions for editor role" do
|
|
571
|
+
user = DecisionAgent::Auth::User.new(
|
|
572
|
+
id: "user2",
|
|
573
|
+
email: "editor@example.com",
|
|
574
|
+
password: "password123",
|
|
575
|
+
roles: [:editor]
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
expect(adapter.can?(user, :read)).to be true
|
|
579
|
+
expect(adapter.can?(user, :write)).to be true
|
|
580
|
+
expect(adapter.can?(user, :delete)).to be false
|
|
581
|
+
expect(adapter.can?(user, :manage_users)).to be false
|
|
582
|
+
expect(adapter.can?(user, :audit)).to be false
|
|
583
|
+
end
|
|
584
|
+
|
|
585
|
+
it "checks permissions for viewer role" do
|
|
586
|
+
user = DecisionAgent::Auth::User.new(
|
|
587
|
+
id: "user3",
|
|
588
|
+
email: "viewer@example.com",
|
|
589
|
+
password: "password123",
|
|
590
|
+
roles: [:viewer]
|
|
591
|
+
)
|
|
592
|
+
|
|
593
|
+
expect(adapter.can?(user, :read)).to be true
|
|
594
|
+
expect(adapter.can?(user, :write)).to be false
|
|
595
|
+
expect(adapter.can?(user, :delete)).to be false
|
|
596
|
+
expect(adapter.can?(user, :manage_users)).to be false
|
|
597
|
+
end
|
|
598
|
+
|
|
599
|
+
it "checks permissions for approver role" do
|
|
600
|
+
user = DecisionAgent::Auth::User.new(
|
|
601
|
+
id: "user4",
|
|
602
|
+
email: "approver@example.com",
|
|
603
|
+
password: "password123",
|
|
604
|
+
roles: [:approver]
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
expect(adapter.can?(user, :read)).to be true
|
|
608
|
+
expect(adapter.can?(user, :approve)).to be true
|
|
609
|
+
expect(adapter.can?(user, :write)).to be false
|
|
610
|
+
expect(adapter.can?(user, :delete)).to be false
|
|
611
|
+
end
|
|
612
|
+
|
|
613
|
+
it "checks permissions for auditor role" do
|
|
614
|
+
user = DecisionAgent::Auth::User.new(
|
|
615
|
+
id: "user5",
|
|
616
|
+
email: "auditor@example.com",
|
|
617
|
+
password: "password123",
|
|
618
|
+
roles: [:auditor]
|
|
619
|
+
)
|
|
620
|
+
|
|
621
|
+
expect(adapter.can?(user, :read)).to be true
|
|
622
|
+
expect(adapter.can?(user, :audit)).to be true
|
|
623
|
+
expect(adapter.can?(user, :write)).to be false
|
|
624
|
+
expect(adapter.can?(user, :delete)).to be false
|
|
625
|
+
end
|
|
626
|
+
|
|
627
|
+
it "checks permissions for user with multiple roles" do
|
|
628
|
+
user = DecisionAgent::Auth::User.new(
|
|
629
|
+
id: "user6",
|
|
630
|
+
email: "multi@example.com",
|
|
631
|
+
password: "password123",
|
|
632
|
+
roles: %i[viewer editor]
|
|
633
|
+
)
|
|
634
|
+
|
|
635
|
+
# Should have permissions from both roles
|
|
636
|
+
expect(adapter.can?(user, :read)).to be true # from viewer
|
|
637
|
+
expect(adapter.can?(user, :write)).to be true # from editor
|
|
638
|
+
expect(adapter.can?(user, :delete)).to be false
|
|
639
|
+
end
|
|
640
|
+
|
|
641
|
+
it "checks role membership using real Role class" do
|
|
642
|
+
admin_user = DecisionAgent::Auth::User.new(
|
|
643
|
+
id: "admin1",
|
|
644
|
+
email: "admin@example.com",
|
|
645
|
+
password: "password123",
|
|
646
|
+
roles: [:admin]
|
|
647
|
+
)
|
|
648
|
+
|
|
649
|
+
expect(adapter.has_role?(admin_user, :admin)).to be true
|
|
650
|
+
expect(adapter.has_role?(admin_user, :editor)).to be false
|
|
651
|
+
expect(adapter.has_role?(admin_user, :viewer)).to be false
|
|
652
|
+
|
|
653
|
+
editor_user = DecisionAgent::Auth::User.new(
|
|
654
|
+
id: "editor1",
|
|
655
|
+
email: "editor@example.com",
|
|
656
|
+
password: "password123",
|
|
657
|
+
roles: [:editor]
|
|
658
|
+
)
|
|
659
|
+
|
|
660
|
+
expect(adapter.has_role?(editor_user, :editor)).to be true
|
|
661
|
+
expect(adapter.has_role?(editor_user, :admin)).to be false
|
|
662
|
+
end
|
|
663
|
+
|
|
664
|
+
it "checks role membership for users with multiple roles" do
|
|
665
|
+
user = DecisionAgent::Auth::User.new(
|
|
666
|
+
id: "multi1",
|
|
667
|
+
email: "multi@example.com",
|
|
668
|
+
password: "password123",
|
|
669
|
+
roles: %i[admin editor]
|
|
670
|
+
)
|
|
671
|
+
|
|
672
|
+
expect(adapter.has_role?(user, :admin)).to be true
|
|
673
|
+
expect(adapter.has_role?(user, :editor)).to be true
|
|
674
|
+
expect(adapter.has_role?(user, :viewer)).to be false
|
|
675
|
+
end
|
|
676
|
+
|
|
677
|
+
it "handles dynamic role assignment" do
|
|
678
|
+
user = DecisionAgent::Auth::User.new(
|
|
679
|
+
id: "dynamic1",
|
|
680
|
+
email: "dynamic@example.com",
|
|
681
|
+
password: "password123",
|
|
682
|
+
roles: []
|
|
683
|
+
)
|
|
684
|
+
|
|
685
|
+
expect(adapter.has_role?(user, :admin)).to be false
|
|
686
|
+
expect(adapter.can?(user, :read)).to be false
|
|
687
|
+
|
|
688
|
+
# Assign role dynamically
|
|
689
|
+
user.assign_role(:viewer)
|
|
690
|
+
|
|
691
|
+
expect(adapter.has_role?(user, :viewer)).to be true
|
|
692
|
+
expect(adapter.can?(user, :read)).to be true
|
|
693
|
+
expect(adapter.can?(user, :write)).to be false
|
|
694
|
+
end
|
|
695
|
+
|
|
696
|
+
it "handles role removal" do
|
|
697
|
+
user = DecisionAgent::Auth::User.new(
|
|
698
|
+
id: "remove1",
|
|
699
|
+
email: "remove@example.com",
|
|
700
|
+
password: "password123",
|
|
701
|
+
roles: %i[admin editor]
|
|
702
|
+
)
|
|
703
|
+
|
|
704
|
+
expect(adapter.has_role?(user, :admin)).to be true
|
|
705
|
+
expect(adapter.can?(user, :manage_users)).to be true
|
|
706
|
+
|
|
707
|
+
# Remove admin role
|
|
708
|
+
user.remove_role(:admin)
|
|
709
|
+
|
|
710
|
+
expect(adapter.has_role?(user, :admin)).to be false
|
|
711
|
+
expect(adapter.has_role?(user, :editor)).to be true
|
|
712
|
+
expect(adapter.can?(user, :manage_users)).to be false
|
|
713
|
+
expect(adapter.can?(user, :write)).to be true # Still has editor role
|
|
714
|
+
end
|
|
715
|
+
|
|
716
|
+
it "checks active status with real User objects" do
|
|
717
|
+
active_user = DecisionAgent::Auth::User.new(
|
|
718
|
+
id: "active1",
|
|
719
|
+
email: "active@example.com",
|
|
720
|
+
password: "password123",
|
|
721
|
+
roles: [:admin],
|
|
722
|
+
active: true
|
|
723
|
+
)
|
|
724
|
+
|
|
725
|
+
expect(adapter.active?(active_user)).to be true
|
|
726
|
+
expect(adapter.can?(active_user, :read)).to be true
|
|
727
|
+
|
|
728
|
+
inactive_user = DecisionAgent::Auth::User.new(
|
|
729
|
+
id: "inactive1",
|
|
730
|
+
email: "inactive@example.com",
|
|
731
|
+
password: "password123",
|
|
732
|
+
roles: [:admin],
|
|
733
|
+
active: false
|
|
734
|
+
)
|
|
735
|
+
|
|
736
|
+
expect(adapter.active?(inactive_user)).to be false
|
|
737
|
+
expect(adapter.can?(inactive_user, :read)).to be false # Inactive users can't do anything
|
|
738
|
+
end
|
|
739
|
+
|
|
740
|
+
it "gets user ID and email from real User objects" do
|
|
741
|
+
user = DecisionAgent::Auth::User.new(
|
|
742
|
+
id: "userid123",
|
|
743
|
+
email: "real@example.com",
|
|
744
|
+
password: "password123",
|
|
745
|
+
roles: [:admin]
|
|
746
|
+
)
|
|
747
|
+
|
|
748
|
+
expect(adapter.user_id(user)).to eq("userid123")
|
|
749
|
+
expect(adapter.user_email(user)).to eq("real@example.com")
|
|
750
|
+
end
|
|
751
|
+
|
|
752
|
+
it "verifies permission checking integrates with real Role.has_permission? method" do
|
|
753
|
+
# Test that the adapter correctly uses Role.has_permission? for all defined roles
|
|
754
|
+
roles_to_test = DecisionAgent::Auth::Role.all
|
|
755
|
+
|
|
756
|
+
roles_to_test.each do |role|
|
|
757
|
+
user = DecisionAgent::Auth::User.new(
|
|
758
|
+
id: "role_test_#{role}",
|
|
759
|
+
email: "#{role}@example.com",
|
|
760
|
+
password: "password123",
|
|
761
|
+
roles: [role]
|
|
762
|
+
)
|
|
763
|
+
|
|
764
|
+
# Get expected permissions from Role class
|
|
765
|
+
expected_permissions = DecisionAgent::Auth::Role.permissions_for(role)
|
|
766
|
+
|
|
767
|
+
# Verify adapter returns same permissions
|
|
768
|
+
all_permissions = %i[read write delete approve deploy manage_users audit]
|
|
769
|
+
all_permissions.each do |permission|
|
|
770
|
+
expected = expected_permissions.include?(permission)
|
|
771
|
+
actual = adapter.can?(user, permission)
|
|
772
|
+
expect(actual).to eq(expected), "Role #{role} permission #{permission} mismatch: expected #{expected}, got #{actual}"
|
|
773
|
+
end
|
|
774
|
+
end
|
|
775
|
+
end
|
|
776
|
+
end
|
|
777
|
+
end
|
|
550
778
|
end
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "decision_agent/dmn/decision_graph"
|
|
5
|
+
|
|
6
|
+
RSpec.describe DecisionAgent::Dmn::DecisionGraph do
|
|
7
|
+
describe "graph construction" do
|
|
8
|
+
let(:graph) do
|
|
9
|
+
described_class.new(id: "graph1", name: "Test Graph")
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it "creates an empty graph" do
|
|
13
|
+
expect(graph.decisions).to be_empty
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it "adds decisions to the graph" do
|
|
17
|
+
decision = DecisionAgent::Dmn::DecisionNode.new(
|
|
18
|
+
id: "decision1",
|
|
19
|
+
name: "First Decision"
|
|
20
|
+
)
|
|
21
|
+
graph.add_decision(decision)
|
|
22
|
+
|
|
23
|
+
expect(graph.decisions["decision1"]).to eq(decision)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it "retrieves decisions by id" do
|
|
27
|
+
decision = DecisionAgent::Dmn::DecisionNode.new(id: "decision1", name: "Test")
|
|
28
|
+
graph.add_decision(decision)
|
|
29
|
+
|
|
30
|
+
retrieved = graph.get_decision("decision1")
|
|
31
|
+
expect(retrieved).to eq(decision)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
describe "decision dependencies" do
|
|
36
|
+
let(:graph) do
|
|
37
|
+
graph = described_class.new(id: "dep_graph", name: "Dependency Graph")
|
|
38
|
+
|
|
39
|
+
# Create decisions
|
|
40
|
+
decision1 = DecisionAgent::Dmn::DecisionNode.new(
|
|
41
|
+
id: "base_rate",
|
|
42
|
+
name: "Base Rate",
|
|
43
|
+
decision_logic: 0.05
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
decision2 = DecisionAgent::Dmn::DecisionNode.new(
|
|
47
|
+
id: "risk_adjustment",
|
|
48
|
+
name: "Risk Adjustment",
|
|
49
|
+
decision_logic: 0.02
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
decision3 = DecisionAgent::Dmn::DecisionNode.new(
|
|
53
|
+
id: "final_rate",
|
|
54
|
+
name: "Final Rate",
|
|
55
|
+
decision_logic: ->(context) { context["base_rate"] + context["risk_adjustment"] }
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# Add dependencies
|
|
59
|
+
decision3.add_dependency("base_rate")
|
|
60
|
+
decision3.add_dependency("risk_adjustment")
|
|
61
|
+
|
|
62
|
+
graph.add_decision(decision1)
|
|
63
|
+
graph.add_decision(decision2)
|
|
64
|
+
graph.add_decision(decision3)
|
|
65
|
+
|
|
66
|
+
graph
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it "tracks decision dependencies" do
|
|
70
|
+
decision = graph.get_decision("final_rate")
|
|
71
|
+
expect(decision.information_requirements.length).to eq(2)
|
|
72
|
+
expect(decision.depends_on?("base_rate")).to be true
|
|
73
|
+
expect(decision.depends_on?("risk_adjustment")).to be true
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it "evaluates decision with dependencies" do
|
|
77
|
+
result = graph.evaluate("final_rate", {})
|
|
78
|
+
expect(result).to eq(0.07) # 0.05 + 0.02
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
describe "topological ordering" do
|
|
83
|
+
let(:graph) do
|
|
84
|
+
graph = described_class.new(id: "topo_graph", name: "Topological Graph")
|
|
85
|
+
|
|
86
|
+
# Create a dependency chain: A -> B -> C
|
|
87
|
+
decision_a = DecisionAgent::Dmn::DecisionNode.new(id: "a", name: "A", decision_logic: 1)
|
|
88
|
+
decision_b = DecisionAgent::Dmn::DecisionNode.new(id: "b", name: "B", decision_logic: ->(ctx) { ctx["a"] + 1 })
|
|
89
|
+
decision_c = DecisionAgent::Dmn::DecisionNode.new(id: "c", name: "C", decision_logic: ->(ctx) { ctx["b"] + 1 })
|
|
90
|
+
|
|
91
|
+
decision_b.add_dependency("a")
|
|
92
|
+
decision_c.add_dependency("b")
|
|
93
|
+
|
|
94
|
+
graph.add_decision(decision_a)
|
|
95
|
+
graph.add_decision(decision_b)
|
|
96
|
+
graph.add_decision(decision_c)
|
|
97
|
+
|
|
98
|
+
graph
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it "returns decisions in topological order" do
|
|
102
|
+
order = graph.topological_order
|
|
103
|
+
expect(order.index("a")).to be < order.index("b")
|
|
104
|
+
expect(order.index("b")).to be < order.index("c")
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
it "identifies root decisions" do
|
|
108
|
+
roots = graph.root_decisions
|
|
109
|
+
expect(roots).to eq(["a"])
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
it "identifies leaf decisions" do
|
|
113
|
+
leaves = graph.leaf_decisions
|
|
114
|
+
expect(leaves).to eq(["c"])
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
describe "circular dependency detection" do
|
|
119
|
+
it "detects circular dependencies" do
|
|
120
|
+
graph = described_class.new(id: "circular", name: "Circular Graph")
|
|
121
|
+
|
|
122
|
+
decision_a = DecisionAgent::Dmn::DecisionNode.new(id: "a", name: "A")
|
|
123
|
+
decision_b = DecisionAgent::Dmn::DecisionNode.new(id: "b", name: "B")
|
|
124
|
+
|
|
125
|
+
decision_a.add_dependency("b")
|
|
126
|
+
decision_b.add_dependency("a")
|
|
127
|
+
|
|
128
|
+
graph.add_decision(decision_a)
|
|
129
|
+
graph.add_decision(decision_b)
|
|
130
|
+
|
|
131
|
+
expect(graph.circular_dependencies?).to be true
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
it "raises error when evaluating circular dependencies" do
|
|
135
|
+
graph = described_class.new(id: "circular", name: "Circular Graph")
|
|
136
|
+
|
|
137
|
+
decision_a = DecisionAgent::Dmn::DecisionNode.new(id: "a", name: "A")
|
|
138
|
+
decision_b = DecisionAgent::Dmn::DecisionNode.new(id: "b", name: "B")
|
|
139
|
+
|
|
140
|
+
decision_a.add_dependency("b")
|
|
141
|
+
decision_b.add_dependency("a")
|
|
142
|
+
|
|
143
|
+
graph.add_decision(decision_a)
|
|
144
|
+
graph.add_decision(decision_b)
|
|
145
|
+
|
|
146
|
+
expect { graph.topological_order }.to raise_error(DecisionAgent::Dmn::DmnError, /Circular dependency/)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
describe "complex graph evaluation" do
|
|
151
|
+
let(:graph) do
|
|
152
|
+
# Build a loan approval graph
|
|
153
|
+
# Decisions: income_check -> credit_check -> final_decision
|
|
154
|
+
graph = described_class.new(id: "loan_graph", name: "Loan Approval Graph")
|
|
155
|
+
|
|
156
|
+
income_check = DecisionAgent::Dmn::DecisionNode.new(
|
|
157
|
+
id: "income_check",
|
|
158
|
+
name: "Income Check",
|
|
159
|
+
decision_logic: ->(ctx) { ctx["income"] >= 50_000 ? "sufficient" : "insufficient" }
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
credit_check = DecisionAgent::Dmn::DecisionNode.new(
|
|
163
|
+
id: "credit_check",
|
|
164
|
+
name: "Credit Check",
|
|
165
|
+
decision_logic: ->(ctx) { ctx["credit_score"] >= 650 ? "good" : "poor" }
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
final_decision = DecisionAgent::Dmn::DecisionNode.new(
|
|
169
|
+
id: "final_decision",
|
|
170
|
+
name: "Final Decision",
|
|
171
|
+
decision_logic: lambda do |ctx|
|
|
172
|
+
if ctx["income_check"] == "sufficient" && ctx["credit_check"] == "good"
|
|
173
|
+
"Approved"
|
|
174
|
+
else
|
|
175
|
+
"Rejected"
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
final_decision.add_dependency("income_check", "income_check")
|
|
181
|
+
final_decision.add_dependency("credit_check", "credit_check")
|
|
182
|
+
|
|
183
|
+
graph.add_decision(income_check)
|
|
184
|
+
graph.add_decision(credit_check)
|
|
185
|
+
graph.add_decision(final_decision)
|
|
186
|
+
|
|
187
|
+
graph
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
it "evaluates graph with all dependencies for approved case" do
|
|
191
|
+
context = { income: 60_000, credit_score: 700 }
|
|
192
|
+
result = graph.evaluate("final_decision", context)
|
|
193
|
+
expect(result).to eq("Approved")
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
it "evaluates graph with all dependencies for rejected case" do
|
|
197
|
+
context = { income: 40_000, credit_score: 600 }
|
|
198
|
+
result = graph.evaluate("final_decision", context)
|
|
199
|
+
expect(result).to eq("Rejected")
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
it "evaluates all decisions in graph" do
|
|
203
|
+
context = { income: 60_000, credit_score: 700 }
|
|
204
|
+
results = graph.evaluate_all(context)
|
|
205
|
+
|
|
206
|
+
expect(results["income_check"]).to eq("sufficient")
|
|
207
|
+
expect(results["credit_check"]).to eq("good")
|
|
208
|
+
expect(results["final_decision"]).to eq("Approved")
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
describe "graph analysis" do
|
|
213
|
+
let(:graph) do
|
|
214
|
+
graph = described_class.new(id: "analysis", name: "Analysis Graph")
|
|
215
|
+
|
|
216
|
+
d1 = DecisionAgent::Dmn::DecisionNode.new(id: "d1", name: "D1", decision_logic: 1)
|
|
217
|
+
d2 = DecisionAgent::Dmn::DecisionNode.new(id: "d2", name: "D2", decision_logic: 2)
|
|
218
|
+
d3 = DecisionAgent::Dmn::DecisionNode.new(id: "d3", name: "D3", decision_logic: 3)
|
|
219
|
+
|
|
220
|
+
d3.add_dependency("d1")
|
|
221
|
+
d3.add_dependency("d2")
|
|
222
|
+
|
|
223
|
+
graph.add_decision(d1)
|
|
224
|
+
graph.add_decision(d2)
|
|
225
|
+
graph.add_decision(d3)
|
|
226
|
+
|
|
227
|
+
graph
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
it "exports dependency graph structure" do
|
|
231
|
+
dep_graph = graph.dependency_graph
|
|
232
|
+
expect(dep_graph["d1"]).to eq([])
|
|
233
|
+
expect(dep_graph["d2"]).to eq([])
|
|
234
|
+
expect(dep_graph["d3"]).to contain_exactly("d1", "d2")
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
it "exports graph to hash representation" do
|
|
238
|
+
hash = graph.to_h
|
|
239
|
+
expect(hash[:id]).to eq("analysis")
|
|
240
|
+
expect(hash[:name]).to eq("Analysis Graph")
|
|
241
|
+
expect(hash[:decisions].keys).to contain_exactly("d1", "d2", "d3")
|
|
242
|
+
expect(hash[:dependency_graph]).to be_a(Hash)
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
describe DecisionAgent::Dmn::DecisionNode do
|
|
247
|
+
it "creates decision node with basic attributes" do
|
|
248
|
+
node = described_class.new(id: "test", name: "Test Decision")
|
|
249
|
+
expect(node.id).to eq("test")
|
|
250
|
+
expect(node.name).to eq("Test Decision")
|
|
251
|
+
expect(node.information_requirements).to be_empty
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
it "adds dependencies" do
|
|
255
|
+
node = described_class.new(id: "test", name: "Test")
|
|
256
|
+
node.add_dependency("dep1", "variable1")
|
|
257
|
+
|
|
258
|
+
expect(node.information_requirements.length).to eq(1)
|
|
259
|
+
expect(node.information_requirements.first[:decision_id]).to eq("dep1")
|
|
260
|
+
expect(node.information_requirements.first[:variable_name]).to eq("variable1")
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
it "checks if node depends on another decision" do
|
|
264
|
+
node = described_class.new(id: "test", name: "Test")
|
|
265
|
+
node.add_dependency("dep1")
|
|
266
|
+
|
|
267
|
+
expect(node.depends_on?("dep1")).to be true
|
|
268
|
+
expect(node.depends_on?("dep2")).to be false
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
it "resets evaluation state" do
|
|
272
|
+
node = described_class.new(id: "test", name: "Test")
|
|
273
|
+
node.value = "some value"
|
|
274
|
+
node.evaluated = true
|
|
275
|
+
|
|
276
|
+
node.reset!
|
|
277
|
+
|
|
278
|
+
expect(node.value).to be_nil
|
|
279
|
+
expect(node.evaluated).to be false
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
end
|