@@ -219,6 +219,145 @@ public function testExceptionBubblesWithoutOnExceptionCallback(): void
219219 $ listener ->listen (1 );
220220 }
221221
222+ public function testStopFromHandlerStopsLoop (): void
223+ {
224+ $ httpClient = $ this ->createMock (HttpClient::class);
225+ $ bot = new Bot ($ httpClient );
226+ $ listener = new BotEventListener ($ bot );
227+
228+ $ pollCount = 0 ;
229+ $ httpClient
230+ ->method ('get ' )
231+ ->willReturnCallback (function () use (&$ pollCount , $ listener ) {
232+ $ pollCount ++;
233+ return [
234+ 'events ' => [
235+ [
236+ 'eventId ' => $ pollCount ,
237+ 'type ' => 'newMessage ' ,
238+ 'payload ' => ['text ' => 'msg ' ],
239+ ],
240+ ],
241+ ];
242+ });
243+
244+ $ listener ->onMessage (function (Bot $ bot , EventDto $ event ) use ($ listener ) {
245+ if ($ event ->eventId === 2 ) {
246+ $ listener ->stop ();
247+ }
248+ });
249+
250+ $ listener ->listen (1 );
251+
252+ $ this ->assertSame (2 , $ pollCount , 'Loop should stop after handler calls stop() ' );
253+ }
254+
255+ public function testUnknownEventTypeIsSkipped (): void
256+ {
257+ $ httpClient = $ this ->createMock (HttpClient::class);
258+ $ bot = new Bot ($ httpClient );
259+ $ listener = new BotEventListener ($ bot );
260+
261+ $ callCount = 0 ;
262+ $ this ->mockEventsGet ($ httpClient , $ listener , $ callCount , [
263+ 'events ' => [
264+ [
265+ 'eventId ' => 1 ,
266+ 'type ' => 'unknownEventType ' ,
267+ 'payload ' => ['text ' => 'hello ' ],
268+ ],
269+ [
270+ 'eventId ' => 2 ,
271+ 'type ' => 'newMessage ' ,
272+ 'payload ' => ['text ' => 'world ' ],
273+ ],
274+ ],
275+ ]);
276+
277+ $ receivedEvents = [];
278+ $ listener ->onMessage (function (Bot $ bot , EventDto $ event ) use (&$ receivedEvents ) {
279+ $ receivedEvents [] = $ event ->eventId ;
280+ });
281+
282+ $ listener ->listen (1 );
283+
284+ $ this ->assertSame ([2 ], $ receivedEvents , 'Unknown event should be skipped, known event processed ' );
285+ }
286+
287+ public function testLastEventIdIsPassedToNextPoll (): void
288+ {
289+ $ httpClient = $ this ->createMock (HttpClient::class);
290+ $ bot = new Bot ($ httpClient );
291+ $ listener = new BotEventListener ($ bot );
292+
293+ $ receivedLastEventIds = [];
294+ $ pollCount = 0 ;
295+ $ httpClient
296+ ->method ('get ' )
297+ ->willReturnCallback (function (string $ path , array $ params ) use (&$ receivedLastEventIds , &$ pollCount , $ listener ) {
298+ $ receivedLastEventIds [] = $ params ['lastEventId ' ];
299+ $ pollCount ++;
300+ if ($ pollCount === 1 ) {
301+ return [
302+ 'events ' => [
303+ ['eventId ' => 42 , 'type ' => 'newMessage ' , 'payload ' => ['text ' => 'hi ' ]],
304+ ],
305+ ];
306+ }
307+ $ listener ->stop ();
308+ return ['events ' => []];
309+ });
310+
311+ $ listener ->onMessage (function () {});
312+ $ listener ->listen (1 );
313+
314+ $ this ->assertSame (0 , $ receivedLastEventIds [0 ], 'First poll should use lastEventId=0 ' );
315+ $ this ->assertSame (42 , $ receivedLastEventIds [1 ], 'Second poll should use lastEventId from previous batch ' );
316+ }
317+
318+ #[\PHPUnit \Framework \Attributes \DataProvider('signalProvider ' )]
319+ public function testStopOnSignal (int $ signal ): void
320+ {
321+ if (!function_exists ('pcntl_signal ' )) {
322+ $ this ->markTestSkipped ('pcntl extension required ' );
323+ }
324+
325+ $ httpClient = $ this ->createMock (HttpClient::class);
326+ $ bot = new Bot ($ httpClient );
327+ $ listener = new BotEventListener ($ bot );
328+
329+ $ pollCount = 0 ;
330+ $ httpClient
331+ ->method ('get ' )
332+ ->willReturnCallback (function () use (&$ pollCount , $ signal ) {
333+ $ pollCount ++;
334+ if ($ pollCount === 1 ) {
335+ posix_kill (posix_getpid (), $ signal );
336+ return [
337+ 'events ' => [
338+ ['eventId ' => 1 , 'type ' => 'newMessage ' , 'payload ' => ['text ' => 'hi ' ]],
339+ ],
340+ ];
341+ }
342+ $ this ->fail ('Loop should have stopped after signal ' );
343+ return ['events ' => []];
344+ });
345+
346+ $ listener ->onMessage (function () {});
347+ $ listener ->listen (1 );
348+
349+ $ this ->assertSame (1 , $ pollCount , 'Loop should stop after signal ' );
350+ }
351+
352+ /**
353+ * @return iterable<string, array{int}>
354+ */
355+ public static function signalProvider (): iterable
356+ {
357+ yield 'SIGTERM ' => [SIGTERM ];
358+ yield 'SIGINT ' => [SIGINT ];
359+ }
360+
222361 /**
223362 * Configure HttpClient mock to return $eventsBatch on first call,
224363 * then empty events + stop() on subsequent calls.
0 commit comments