@@ -1435,6 +1435,101 @@ public function testAwaitStringSignalTimeoutReturnsNullAndReplaysForQueries(): v
14351435 }
14361436 }
14371437
1438+ public function testAwaitStringSignalReceivedBeforeTimeoutFiredWinsByHistoryOrder (): void
1439+ {
1440+ Queue::fake ();
1441+
1442+ try {
1443+ Carbon::setTestNow (Carbon::parse ('2026-04-16 12:00:00 ' ));
1444+
1445+ $ workflow = WorkflowStub::make (TestAwaitSignalTimeoutWorkflow::class, 'await-signal-before-timeout-race ' );
1446+ $ workflow ->start ('approved-by ' , 5 );
1447+
1448+ $ this ->runReadyWorkflowTask ($ workflow ->runId ());
1449+
1450+ $ signal = $ workflow ->attemptSignal ('approved-by ' , 'Jordan ' );
1451+
1452+ $ this ->assertTrue ($ signal ->accepted ());
1453+
1454+ Carbon::setTestNow (now ()->addSeconds (5 ));
1455+
1456+ $ this ->runReadyTimerTask ($ workflow ->runId ());
1457+ $ this ->runReadyWorkflowTask ($ workflow ->runId ());
1458+
1459+ $ this ->assertTrue ($ workflow ->refresh ()->completed ());
1460+ $ this ->assertSame ([
1461+ 'payload ' => 'Jordan ' ,
1462+ 'timed_out ' => false ,
1463+ 'stage ' => 'received ' ,
1464+ 'workflow_id ' => 'await-signal-before-timeout-race ' ,
1465+ 'run_id ' => $ workflow ->runId (),
1466+ ], $ workflow ->output ());
1467+
1468+ $ eventTypes = WorkflowHistoryEvent::query ()
1469+ ->where ('workflow_run_id ' , $ workflow ->runId ())
1470+ ->orderBy ('sequence ' )
1471+ ->pluck ('event_type ' )
1472+ ->map (static fn ($ eventType ) => $ eventType ->value )
1473+ ->all ();
1474+
1475+ $ this ->assertLessThan (
1476+ array_search ('TimerFired ' , $ eventTypes , true ),
1477+ array_search ('SignalReceived ' , $ eventTypes , true ),
1478+ );
1479+ $ this ->assertContains ('SignalApplied ' , $ eventTypes );
1480+ } finally {
1481+ Carbon::setTestNow ();
1482+ }
1483+ }
1484+
1485+ public function testAwaitStringSignalTimeoutFiredBeforeSignalReceivedWinsByHistoryOrder (): void
1486+ {
1487+ Queue::fake ();
1488+
1489+ try {
1490+ Carbon::setTestNow (Carbon::parse ('2026-04-16 12:00:00 ' ));
1491+
1492+ $ workflow = WorkflowStub::make (TestAwaitSignalTimeoutWorkflow::class, 'await-timeout-before-signal-race ' );
1493+ $ workflow ->start ('approved-by ' , 5 );
1494+
1495+ $ this ->runReadyWorkflowTask ($ workflow ->runId ());
1496+
1497+ Carbon::setTestNow (now ()->addSeconds (5 ));
1498+
1499+ $ this ->runReadyTimerTask ($ workflow ->runId ());
1500+
1501+ $ signal = $ workflow ->attemptSignal ('approved-by ' , 'Jordan ' );
1502+
1503+ $ this ->assertTrue ($ signal ->accepted ());
1504+
1505+ $ this ->runReadyWorkflowTask ($ workflow ->runId ());
1506+
1507+ $ this ->assertTrue ($ workflow ->refresh ()->completed ());
1508+ $ this ->assertSame ([
1509+ 'payload ' => null ,
1510+ 'timed_out ' => true ,
1511+ 'stage ' => 'timed-out ' ,
1512+ 'workflow_id ' => 'await-timeout-before-signal-race ' ,
1513+ 'run_id ' => $ workflow ->runId (),
1514+ ], $ workflow ->output ());
1515+
1516+ $ eventTypes = WorkflowHistoryEvent::query ()
1517+ ->where ('workflow_run_id ' , $ workflow ->runId ())
1518+ ->orderBy ('sequence ' )
1519+ ->pluck ('event_type ' )
1520+ ->map (static fn ($ eventType ) => $ eventType ->value )
1521+ ->all ();
1522+
1523+ $ this ->assertLessThan (
1524+ array_search ('SignalReceived ' , $ eventTypes , true ),
1525+ array_search ('TimerFired ' , $ eventTypes , true ),
1526+ );
1527+ $ this ->assertNotContains ('SignalApplied ' , $ eventTypes );
1528+ } finally {
1529+ Carbon::setTestNow ();
1530+ }
1531+ }
1532+
14381533 public function testAwaitStringSignalPreservesEmptyAndMultipleArgumentPayloadRules (): void
14391534 {
14401535 Queue::fake ();
0 commit comments