Skip to content

Commit 80623d8

Browse files
committed
fix workflow global middleware
1 parent 48869e0 commit 80623d8

File tree

2 files changed

+330
-2
lines changed

2 files changed

+330
-2
lines changed

src/Workflow/Workflow.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,10 @@ public function __construct(
8383
$this->workflowId = $resumeToken ?? uniqid('workflow_');
8484

8585
// Register the node middleware
86-
$global = $this->globalMiddleware();
86+
$this->addGlobalMiddleware($this->globalMiddleware());
8787
foreach ($this->middleware() as $node => $middleware) {
8888
$middleware = is_array($middleware) ? $middleware : [$middleware];
89-
$this->addMiddleware($node, array_merge($middleware, $global));
89+
$this->addMiddleware($node, $middleware);
9090
}
9191
}
9292

Lines changed: 328 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,328 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace NeuronAI\Tests\Workflow;
6+
7+
use NeuronAI\Testing\FakeMiddleware;
8+
use NeuronAI\Tests\Workflow\Stubs\NodeOne;
9+
use NeuronAI\Tests\Workflow\Stubs\NodeThree;
10+
use NeuronAI\Tests\Workflow\Stubs\NodeTwo;
11+
use NeuronAI\Workflow\Events\Event;
12+
use NeuronAI\Workflow\Workflow;
13+
use PHPUnit\Framework\TestCase;
14+
15+
class GlobalMiddlewareMethodTest extends TestCase
16+
{
17+
public function testGlobalMiddlewareOverrideRunsOnAllNodes(): void
18+
{
19+
$middleware = FakeMiddleware::make();
20+
21+
// Test using a workflow class that overrides globalMiddleware()
22+
$workflow = new class ($middleware) extends Workflow {
23+
public function __construct(
24+
private readonly FakeMiddleware $middleware,
25+
) {
26+
parent::__construct();
27+
}
28+
29+
protected function nodes(): array
30+
{
31+
return [new NodeOne(), new NodeTwo(), new NodeThree()];
32+
}
33+
34+
protected function globalMiddleware(): array
35+
{
36+
return [$this->middleware];
37+
}
38+
};
39+
40+
$workflow->init()->run();
41+
42+
// 3 nodes = 3 before + 3 after
43+
$middleware->assertBeforeCalledTimes(3);
44+
$middleware->assertAfterCalledTimes(3);
45+
$middleware->assertCallCount(6);
46+
}
47+
48+
public function testGlobalMiddlewareOverrideRunsWhenMiddlewareReturnsEmpty(): void
49+
{
50+
$middleware = FakeMiddleware::make();
51+
52+
// Even without defining middleware(), globalMiddleware() should work
53+
$workflow = new class ($middleware) extends Workflow {
54+
public function __construct(
55+
private readonly FakeMiddleware $middleware,
56+
) {
57+
parent::__construct();
58+
}
59+
60+
protected function nodes(): array
61+
{
62+
return [new NodeOne(), new NodeTwo(), new NodeThree()];
63+
}
64+
65+
protected function globalMiddleware(): array
66+
{
67+
return [$this->middleware];
68+
}
69+
70+
// Note: middleware() not overridden (returns [])
71+
};
72+
73+
$workflow->init()->run();
74+
75+
// Should still run on all 3 nodes
76+
$middleware->assertBeforeCalledTimes(3);
77+
$middleware->assertAfterCalledTimes(3);
78+
}
79+
80+
public function testGlobalMiddlewareOverrideCombinesWithNodeMiddlewareOverride(): void
81+
{
82+
$global = FakeMiddleware::make();
83+
$nodeSpecific = FakeMiddleware::make();
84+
85+
$workflow = new class ($global, $nodeSpecific) extends Workflow {
86+
public function __construct(
87+
private readonly FakeMiddleware $global,
88+
private readonly FakeMiddleware $nodeSpecific,
89+
) {
90+
parent::__construct();
91+
}
92+
93+
protected function nodes(): array
94+
{
95+
return [new NodeOne(), new NodeTwo(), new NodeThree()];
96+
}
97+
98+
protected function globalMiddleware(): array
99+
{
100+
return [$this->global];
101+
}
102+
103+
protected function middleware(): array
104+
{
105+
return [
106+
NodeTwo::class => $this->nodeSpecific,
107+
];
108+
}
109+
};
110+
111+
$workflow->init()->run();
112+
113+
// Global middleware runs on all 3 nodes
114+
$global->assertBeforeCalledTimes(3);
115+
$global->assertAfterCalledTimes(3);
116+
117+
// Node-specific middleware runs only on NodeTwo
118+
$nodeSpecific->assertBeforeCalledTimes(1);
119+
$nodeSpecific->assertAfterCalledTimes(1);
120+
}
121+
122+
public function testGlobalMiddlewareOverrideExecutesInCorrectOrder(): void
123+
{
124+
$order = [];
125+
126+
$global = FakeMiddleware::make()
127+
->setBeforeHandler(function () use (&$order): void {
128+
$order[] = 'global.before';
129+
})
130+
->setAfterHandler(function () use (&$order): void {
131+
$order[] = 'global.after';
132+
});
133+
134+
$node = FakeMiddleware::make()
135+
->setBeforeHandler(function () use (&$order): void {
136+
$order[] = 'node.before';
137+
})
138+
->setAfterHandler(function () use (&$order): void {
139+
$order[] = 'node.after';
140+
});
141+
142+
$workflow = new class ($global, $node) extends Workflow {
143+
public function __construct(
144+
private readonly FakeMiddleware $global,
145+
private readonly FakeMiddleware $node,
146+
) {
147+
parent::__construct();
148+
}
149+
150+
protected function nodes(): array
151+
{
152+
return [new NodeOne(), new NodeTwo(), new NodeThree()];
153+
}
154+
155+
protected function globalMiddleware(): array
156+
{
157+
return [$this->global];
158+
}
159+
160+
protected function middleware(): array
161+
{
162+
return [
163+
NodeOne::class => $this->node,
164+
];
165+
}
166+
};
167+
168+
$workflow->init()->run();
169+
170+
// Order per node: global.before → node.before → [node] → global.after → node.after
171+
// NodeOne has node-specific middleware, NodeTwo/Three only have global
172+
$this->assertSame([
173+
// NodeOne
174+
'global.before', 'node.before', 'global.after', 'node.after',
175+
// NodeTwo (only global)
176+
'global.before', 'global.after',
177+
// NodeThree (only global)
178+
'global.before', 'global.after',
179+
], $order);
180+
}
181+
182+
public function testGlobalMiddlewareOverrideReceivesCorrectEvents(): void
183+
{
184+
$middleware = FakeMiddleware::make();
185+
186+
$workflow = new class ($middleware) extends Workflow {
187+
public function __construct(
188+
private readonly FakeMiddleware $middleware,
189+
) {
190+
parent::__construct();
191+
}
192+
193+
protected function nodes(): array
194+
{
195+
return [new NodeOne(), new NodeTwo(), new NodeThree()];
196+
}
197+
198+
protected function globalMiddleware(): array
199+
{
200+
return [$this->middleware];
201+
}
202+
};
203+
204+
$workflow->init()->run();
205+
206+
$beforeRecords = $middleware->getBeforeRecords();
207+
208+
// Each before() should receive the correct event for that node
209+
$this->assertCount(3, $beforeRecords);
210+
$this->assertInstanceOf(Event::class, $beforeRecords[0]->event);
211+
$this->assertInstanceOf(Event::class, $beforeRecords[1]->event);
212+
$this->assertInstanceOf(Event::class, $beforeRecords[2]->event);
213+
214+
$afterRecords = $middleware->getAfterRecords();
215+
$this->assertCount(3, $afterRecords);
216+
}
217+
218+
public function testGlobalMiddlewareOverrideCanReadAndWriteState(): void
219+
{
220+
$middleware = FakeMiddleware::make()
221+
->setBeforeHandler(function ($node, $event, $state): void {
222+
$state->set('injected_by_global', true);
223+
$state->set('execution_count', ($state->get('execution_count') ?? 0) + 1);
224+
});
225+
226+
$workflow = new class ($middleware) extends Workflow {
227+
public function __construct(
228+
private readonly FakeMiddleware $middleware,
229+
) {
230+
parent::__construct();
231+
}
232+
233+
protected function nodes(): array
234+
{
235+
return [new NodeOne(), new NodeTwo(), new NodeThree()];
236+
}
237+
238+
protected function globalMiddleware(): array
239+
{
240+
return [$this->middleware];
241+
}
242+
};
243+
244+
$finalState = $workflow->init()->run();
245+
246+
$this->assertTrue($finalState->get('injected_by_global'));
247+
$this->assertEquals(3, $finalState->get('execution_count'));
248+
}
249+
250+
public function testEmptyGlobalMiddlewareOverrideDoesNotCauseErrors(): void
251+
{
252+
// A workflow that explicitly returns empty array should work fine
253+
$workflow = new class extends Workflow {
254+
protected function nodes(): array
255+
{
256+
return [new NodeOne(), new NodeTwo(), new NodeThree()];
257+
}
258+
259+
protected function globalMiddleware(): array
260+
{
261+
return [];
262+
}
263+
};
264+
265+
$finalState = $workflow->init()->run();
266+
267+
// Workflow should complete normally
268+
$this->assertTrue($finalState->get('node_one_executed'));
269+
$this->assertTrue($finalState->get('node_two_executed'));
270+
$this->assertTrue($finalState->get('node_three_executed'));
271+
}
272+
273+
public function testMultipleGlobalMiddlewareInOverrideRunInOrder(): void
274+
{
275+
$order = [];
276+
277+
$first = FakeMiddleware::make()
278+
->setBeforeHandler(function () use (&$order): void {
279+
$order[] = 'first.before';
280+
})
281+
->setAfterHandler(function () use (&$order): void {
282+
$order[] = 'first.after';
283+
});
284+
285+
$second = FakeMiddleware::make()
286+
->setBeforeHandler(function () use (&$order): void {
287+
$order[] = 'second.before';
288+
})
289+
->setAfterHandler(function () use (&$order): void {
290+
$order[] = 'second.after';
291+
});
292+
293+
$workflow = new class ($first, $second) extends Workflow {
294+
public function __construct(
295+
private readonly FakeMiddleware $first,
296+
private readonly FakeMiddleware $second,
297+
) {
298+
parent::__construct();
299+
}
300+
301+
protected function nodes(): array
302+
{
303+
return [new NodeOne(), new NodeTwo(), new NodeThree()];
304+
}
305+
306+
protected function globalMiddleware(): array
307+
{
308+
return [$this->first, $this->second];
309+
}
310+
};
311+
312+
$workflow->init()->run();
313+
314+
// Order per node: first.before → second.before → [node] → first.after → second.after
315+
// 3 nodes, so pattern repeats 3 times
316+
$this->assertSame(
317+
[
318+
// NodeOne
319+
'first.before', 'second.before', 'first.after', 'second.after',
320+
// NodeTwo
321+
'first.before', 'second.before', 'first.after', 'second.after',
322+
// NodeThree
323+
'first.before', 'second.before', 'first.after', 'second.after',
324+
],
325+
$order
326+
);
327+
}
328+
}

0 commit comments

Comments
 (0)