1+ <?php
2+
3+ declare (strict_types=1 );
4+
5+ namespace NeuronAI \Tests \Workflow ;
6+
7+ use NeuronAI \Workflow \Edge ;
8+ use NeuronAI \Workflow \Node ;
9+ use NeuronAI \Workflow \Workflow ;
10+ use NeuronAI \Workflow \WorkflowState ;
11+ use PHPUnit \Framework \TestCase ;
12+
13+ // Calculator nodes that can be reused with different values
14+ class AddNode extends Node
15+ {
16+ public function __construct (private int $ value )
17+ {
18+ }
19+
20+ public function run (WorkflowState $ state ): WorkflowState
21+ {
22+ $ current = $ state ->get ('value ' , 0 );
23+ $ state ->set ('value ' , $ current + $ this ->value );
24+ $ history = $ state ->get ('history ' , []);
25+ $ history [] = "Added {$ this ->value }" ;
26+ $ state ->set ('history ' , $ history );
27+ return $ state ;
28+ }
29+ }
30+
31+ class MultiplyNode extends Node
32+ {
33+ public function __construct (private int $ value )
34+ {
35+ }
36+
37+ public function run (WorkflowState $ state ): WorkflowState
38+ {
39+ $ current = $ state ->get ('value ' , 0 );
40+ $ state ->set ('value ' , $ current * $ this ->value );
41+ $ history = $ state ->get ('history ' , []);
42+ $ history [] = "Multiplied by {$ this ->value }" ;
43+ $ state ->set ('history ' , $ history );
44+ return $ state ;
45+ }
46+ }
47+
48+ class SubtractNode extends Node
49+ {
50+ public function __construct (private int $ value )
51+ {
52+ }
53+
54+ public function run (WorkflowState $ state ): WorkflowState
55+ {
56+ $ current = $ state ->get ('value ' , 0 );
57+ $ state ->set ('value ' , $ current - $ this ->value );
58+ $ history = $ state ->get ('history ' , []);
59+ $ history [] = "Subtracted {$ this ->value }" ;
60+ $ state ->set ('history ' , $ history );
61+ return $ state ;
62+ }
63+ }
64+
65+ class FinishEvenNode extends Node
66+ {
67+ public function run (WorkflowState $ state ): WorkflowState
68+ {
69+ $ state ->set ('result_type ' , 'even ' );
70+ return $ state ;
71+ }
72+ }
73+
74+ class FinishOddNode extends Node
75+ {
76+ public function run (WorkflowState $ state ): WorkflowState
77+ {
78+ $ state ->set ('result_type ' , 'odd ' );
79+ return $ state ;
80+ }
81+ }
82+
83+ // Test workflow that uses string keys
84+ class CalculatorWorkflow extends Workflow
85+ {
86+ public function nodes (): array
87+ {
88+ return [
89+ 'add1 ' => new AddNode (1 ),
90+ 'multiply3_first ' => new MultiplyNode (3 ),
91+ 'multiply3_second ' => new MultiplyNode (3 ),
92+ 'sub1 ' => new SubtractNode (1 ),
93+ 'finish_even ' => new FinishEvenNode (),
94+ 'finish_odd ' => new FinishOddNode ()
95+ ];
96+ }
97+
98+ public function edges (): array
99+ {
100+ return [
101+ // ((startingValue + 1) * 3) * 3) - 1
102+ new Edge ('add1 ' , 'multiply3_first ' ),
103+ new Edge ('multiply3_first ' , 'multiply3_second ' ),
104+ new Edge ('multiply3_second ' , 'sub1 ' ),
105+
106+ // Branch based on even/odd
107+ new Edge ('sub1 ' , 'finish_even ' , fn ($ state ) => $ state ->get ('value ' ) % 2 === 0 ),
108+ new Edge ('sub1 ' , 'finish_odd ' , fn ($ state ) => $ state ->get ('value ' ) % 2 !== 0 )
109+ ];
110+ }
111+
112+ protected function start (): string
113+ {
114+ return 'add1 ' ;
115+ }
116+
117+ protected function end (): array
118+ {
119+ return ['finish_even ' , 'finish_odd ' ];
120+ }
121+ }
122+
123+ class WorkflowStringKeysTest extends TestCase
124+ {
125+ public function test_workflow_with_string_keys (): void
126+ {
127+ $ workflow = new CalculatorWorkflow ();
128+
129+ // Test with initial value 2: ((2 + 1) * 3) * 3) - 1 = 26 (even)
130+ $ initialState = new WorkflowState (['value ' => 2 ]);
131+ $ result = $ workflow ->run ($ initialState );
132+
133+ $ this ->assertEquals (26 , $ result ->get ('value ' ));
134+ $ this ->assertEquals ('even ' , $ result ->get ('result_type ' ));
135+ $ this ->assertContains ('Added 1 ' , $ result ->get ('history ' ));
136+ $ this ->assertContains ('Multiplied by 3 ' , $ result ->get ('history ' ));
137+ $ this ->assertContains ('Subtracted 1 ' , $ result ->get ('history ' ));
138+ }
139+
140+ public function test_workflow_with_string_keys_odd_result (): void
141+ {
142+ $ workflow = new CalculatorWorkflow ();
143+
144+ // Test with initial value 1: ((1 + 1) * 3) * 3) - 1 = 17 (odd)
145+ $ initialState = new WorkflowState (['value ' => 1 ]);
146+ $ result = $ workflow ->run ($ initialState );
147+
148+ $ this ->assertEquals (17 , $ result ->get ('value ' ));
149+ $ this ->assertEquals ('odd ' , $ result ->get ('result_type ' ));
150+ }
151+
152+ public function test_programmatic_workflow_with_string_keys (): void
153+ {
154+ $ workflow = new Workflow ();
155+ $ workflow ->addNodes ([
156+ 'add1 ' => new AddNode (1 ),
157+ 'multiply2 ' => new MultiplyNode (2 ),
158+ 'finish_even ' => new FinishEvenNode (),
159+ 'finish_odd ' => new FinishOddNode ()
160+ ])
161+ ->addEdges ([
162+ new Edge ('add1 ' , 'multiply2 ' ),
163+ new Edge ('multiply2 ' , 'finish_even ' , fn ($ state ) => $ state ->get ('value ' ) % 2 === 0 ),
164+ new Edge ('multiply2 ' , 'finish_odd ' , fn ($ state ) => $ state ->get ('value ' ) % 2 !== 0 )
165+ ])
166+ ->setStart ('add1 ' )
167+ ->setEnd ('finish_even ' )
168+ ->setEnd ('finish_odd ' );
169+
170+ // Test with initial value 3: (3 + 1) * 2 = 8 (even)
171+ $ initialState = new WorkflowState (['value ' => 3 ]);
172+ $ result = $ workflow ->run ($ initialState );
173+
174+ $ this ->assertEquals (8 , $ result ->get ('value ' ));
175+ $ this ->assertEquals ('even ' , $ result ->get ('result_type ' ));
176+ }
177+
178+ public function test_mermaid_export_with_string_keys (): void
179+ {
180+ $ workflow = new Workflow ();
181+ $ workflow ->addNodes ([
182+ 'start ' => new AddNode (1 ),
183+ 'middle ' => new MultiplyNode (2 ),
184+ 'finish ' => new FinishEvenNode ()
185+ ])
186+ ->addEdges ([
187+ new Edge ('start ' , 'middle ' ),
188+ new Edge ('middle ' , 'finish ' )
189+ ])
190+ ->setStart ('start ' )
191+ ->setEnd ('finish ' );
192+
193+ $ export = $ workflow ->export ();
194+
195+ $ this ->assertStringContainsString ('start --> middle ' , $ export );
196+ $ this ->assertStringContainsString ('middle --> finish ' , $ export );
197+ }
198+
199+ public function test_backward_compatibility_with_class_names (): void
200+ {
201+ // This test ensures the old behavior still works
202+ $ workflow = new Workflow ();
203+ $ workflow ->addNode (new StartNode ())
204+ ->addNode (new FinishNode ())
205+ ->addEdge (new Edge (StartNode::class, FinishNode::class))
206+ ->setStart (StartNode::class)
207+ ->setEnd (FinishNode::class);
208+
209+ $ result = $ workflow ->run ();
210+
211+ $ this ->assertEquals ('end ' , $ result ->get ('step ' ));
212+ }
213+
214+ public function test_mixed_mode_nodes_and_edges (): void
215+ {
216+ // Test mixing both approaches - indexed array with class name edges
217+ $ workflow = new Workflow ();
218+ $ workflow ->addNodes ([
219+ new StartNode (),
220+ new MiddleNode (),
221+ new FinishNode ()
222+ ])
223+ ->addEdges ([
224+ new Edge (StartNode::class, MiddleNode::class),
225+ new Edge (MiddleNode::class, FinishNode::class)
226+ ])
227+ ->setStart (StartNode::class)
228+ ->setEnd (FinishNode::class);
229+
230+ $ result = $ workflow ->run ();
231+
232+ $ this ->assertEquals ('end ' , $ result ->get ('step ' ));
233+ $ this ->assertEquals (1 , $ result ->get ('counter ' ));
234+ }
235+ }
0 commit comments