From c65b8137b1a6504c9d38c3c8e17a37ec3380226e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Apr 2026 05:42:04 +0000 Subject: [PATCH 1/4] Initial plan From f5c830da7c1968d0e308d699270190a47030c0d5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Apr 2026 05:49:07 +0000 Subject: [PATCH 2/4] fix: preserve later workflow exception replay logs Agent-Logs-Url: https://github.com/durable-workflow/workflow/sessions/d1646bb7-d46e-4808-8a4e-4ebf909aa1f7 Co-authored-by: rmcdaniel <1130888+rmcdaniel@users.noreply.github.com> --- src/Exception.php | 13 ++++++++++- tests/Unit/ExceptionTest.php | 44 ++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/src/Exception.php b/src/Exception.php index de177e51..ff001e5d 100644 --- a/src/Exception.php +++ b/src/Exception.php @@ -53,7 +53,7 @@ public function handle() try { if ($this->storedWorkflow->hasLogByIndex($this->index)) { $workflow->resume(); - } elseif (! $this->storedWorkflow->logs()->where('class', self::class)->exists()) { + } elseif (! $this->previousLogIsException()) { $workflow->next($this->index, $this->now, self::class, $this->exception); } } catch (TransitionNotFound) { @@ -74,4 +74,15 @@ public function middleware() ), ]; } + + private function previousLogIsException(): bool + { + $previousLog = $this->storedWorkflow->logs() + ->reorder() + ->where('index', '<', $this->index) + ->orderByDesc('index') + ->first(); + + return $previousLog?->class === self::class; + } } diff --git a/tests/Unit/ExceptionTest.php b/tests/Unit/ExceptionTest.php index 1f6cf3ca..74650224 100644 --- a/tests/Unit/ExceptionTest.php +++ b/tests/Unit/ExceptionTest.php @@ -10,6 +10,7 @@ use Workflow\Middleware\WithoutOverlappingMiddleware; use Workflow\Models\StoredWorkflow; use Workflow\Serializers\Serializer; +use Workflow\Signal; use Workflow\States\WorkflowRunningStatus; use Workflow\WorkflowStub; @@ -76,4 +77,47 @@ public function testSkipsWriteWhenSiblingExceptionLogExists(): void $this->assertFalse($storedWorkflow->hasLogByIndex(1)); $this->assertSame(1, $storedWorkflow->logs()->count()); } + + public function testWritesLaterExceptionAfterWorkflowAdvances(): void + { + $workflow = WorkflowStub::load(WorkflowStub::make(TestWorkflow::class)->id()); + $storedWorkflow = StoredWorkflow::findOrFail($workflow->id()); + $storedWorkflow->update([ + 'arguments' => Serializer::serialize([]), + 'status' => WorkflowRunningStatus::$name, + ]); + + $storedWorkflow->logs() + ->create([ + 'index' => 0, + 'now' => now() + ->toDateTimeString(), + 'class' => Exception::class, + 'result' => Serializer::serialize([ + 'class' => \RuntimeException::class, + 'message' => 'first failure', + 'code' => 0, + ]), + ]); + + $storedWorkflow->logs() + ->create([ + 'index' => 1, + 'now' => now() + ->toDateTimeString(), + 'class' => Signal::class, + 'result' => Serializer::serialize(null), + ]); + + $exception = new Exception(2, now()->toDateTimeString(), $storedWorkflow, [ + 'class' => \InvalidArgumentException::class, + 'message' => 'second failure', + 'code' => 0, + ]); + $exception->handle(); + + $this->assertTrue($storedWorkflow->hasLogByIndex(2)); + $this->assertSame(3, $storedWorkflow->logs()->count()); + $this->assertSame(Exception::class, $storedWorkflow->findLogByIndex(2)?->class); + } } From 4b5e74cf698f01f4710c3a726250769b434c46b2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Apr 2026 06:46:41 +0000 Subject: [PATCH 3/4] fix: refine exception replay probe for consecutive failures Agent-Logs-Url: https://github.com/durable-workflow/workflow/sessions/400020c3-e0d1-467b-8694-8f8f62b4e78b Co-authored-by: rmcdaniel <1130888+rmcdaniel@users.noreply.github.com> --- src/Activity.php | 3 +- src/ActivityStub.php | 13 ++++++ src/ChildWorkflowStub.php | 13 ++++++ src/Exception.php | 40 ++++++++++++---- src/Workflow.php | 16 ++++++- src/WorkflowStub.php | 5 +- ...TestConsecutiveCaughtExceptionWorkflow.php | 27 +++++++++++ .../TestParallelCaughtExceptionWorkflow.php | 27 +++++++++++ .../TestSignalAdvancedExceptionWorkflow.php | 34 ++++++++++++++ tests/Unit/ExceptionTest.php | 46 +++++++++++++++++-- 10 files changed, 206 insertions(+), 18 deletions(-) create mode 100644 tests/Fixtures/TestConsecutiveCaughtExceptionWorkflow.php create mode 100644 tests/Fixtures/TestParallelCaughtExceptionWorkflow.php create mode 100644 tests/Fixtures/TestSignalAdvancedExceptionWorkflow.php diff --git a/src/Activity.php b/src/Activity.php index a26508a7..44e116b9 100644 --- a/src/Activity.php +++ b/src/Activity.php @@ -170,7 +170,8 @@ public function failed(Throwable $throwable): void $this->storedWorkflow, $throwable, $workflow->connection(), - $workflow->queue() + $workflow->queue(), + $this::class ); } diff --git a/src/ActivityStub.php b/src/ActivityStub.php index ed8f2ca2..1c6ee6bb 100644 --- a/src/ActivityStub.php +++ b/src/ActivityStub.php @@ -74,6 +74,19 @@ public static function make($activity, ...$arguments): PromiseInterface return resolve($result); } + if ($context->replaying && property_exists($context, 'probeIndex')) { + $context->probeMatched = $context->probeIndex === $context->index + && ( + ! property_exists($context, 'probeClass') + || $context->probeClass === null + || $context->probeClass === $activity + ); + ++$context->index; + WorkflowStub::setContext($context); + $deferred = new Deferred(); + return $deferred->promise(); + } + $activity::dispatch($context->index, $context->now, $context->storedWorkflow, ...$arguments); ++$context->index; diff --git a/src/ChildWorkflowStub.php b/src/ChildWorkflowStub.php index 896237a3..b3b2fbe8 100644 --- a/src/ChildWorkflowStub.php +++ b/src/ChildWorkflowStub.php @@ -67,6 +67,19 @@ public static function make($workflow, ...$arguments): PromiseInterface return resolve($result); } + if ($context->replaying && property_exists($context, 'probeIndex')) { + $context->probeMatched = $context->probeIndex === $context->index + && ( + ! property_exists($context, 'probeClass') + || $context->probeClass === null + || $context->probeClass === $workflow + ); + ++$context->index; + WorkflowStub::setContext($context); + $deferred = new Deferred(); + return $deferred->promise(); + } + if (! $context->replaying) { $storedChildWorkflow = $context->storedWorkflow->children() ->wherePivot('parent_index', $context->index) diff --git a/src/Exception.php b/src/Exception.php index ff001e5d..d7ec9376 100644 --- a/src/Exception.php +++ b/src/Exception.php @@ -35,7 +35,8 @@ public function __construct( public StoredWorkflow $storedWorkflow, public $exception, $connection = null, - $queue = null + $queue = null, + public ?string $sourceClass = null ) { $connection = $connection ?? $this->storedWorkflow->effectiveConnection() ?? config('queue.default'); $queue = $queue ?? $this->storedWorkflow->effectiveQueue() ?? config( @@ -53,7 +54,7 @@ public function handle() try { if ($this->storedWorkflow->hasLogByIndex($this->index)) { $workflow->resume(); - } elseif (! $this->previousLogIsException()) { + } elseif ($this->isCurrentReplayFrontier()) { $workflow->next($this->index, $this->now, self::class, $this->exception); } } catch (TransitionNotFound) { @@ -75,14 +76,35 @@ public function middleware() ]; } - private function previousLogIsException(): bool + private function isCurrentReplayFrontier(): bool { - $previousLog = $this->storedWorkflow->logs() - ->reorder() - ->where('index', '<', $this->index) - ->orderByDesc('index') - ->first(); + $workflowClass = $this->storedWorkflow->class; - return $previousLog?->class === self::class; + if (! is_string($workflowClass) || $workflowClass === '') { + return true; + } + + $workflow = new $workflowClass($this->storedWorkflow, ...$this->storedWorkflow->workflowArguments()); + $workflow->replaying = true; + + $previousContext = WorkflowStub::getContext(); + + WorkflowStub::setContext([ + 'storedWorkflow' => $this->storedWorkflow, + 'index' => 0, + 'now' => $this->now, + 'replaying' => true, + 'probeIndex' => $this->index, + 'probeClass' => $this->sourceClass, + 'probeMatched' => false, + ]); + + try { + $workflow->handle(); + + return WorkflowStub::getContext()->probeMatched; + } finally { + WorkflowStub::setContext($previousContext); + } } } diff --git a/src/Workflow.php b/src/Workflow.php index 20b80b6e..994b4d18 100644 --- a/src/Workflow.php +++ b/src/Workflow.php @@ -209,7 +209,7 @@ public function handle(): void $this->now = $log ? $log->now : Carbon::now(); } - WorkflowStub::setContext([ + $this->setContext([ 'storedWorkflow' => $this->storedWorkflow, 'index' => $this->index, 'now' => $this->now, @@ -229,7 +229,7 @@ public function handle(): void $this->now = $log ? $log->now : Carbon::now(); - WorkflowStub::setContext([ + $this->setContext([ 'storedWorkflow' => $this->storedWorkflow, 'index' => $this->index, 'now' => $this->now, @@ -309,4 +309,16 @@ public function handle(): void } } } + + private function setContext(array $context): void + { + $existingContext = WorkflowStub::getContext(); + + if (property_exists($existingContext, 'probeIndex')) { + $context['probeIndex'] = $existingContext->probeIndex; + $context['probeMatched'] = $existingContext->probeMatched ?? false; + } + + WorkflowStub::setContext($context); + } } diff --git a/src/WorkflowStub.php b/src/WorkflowStub.php index 5aabead1..bdb344c4 100644 --- a/src/WorkflowStub.php +++ b/src/WorkflowStub.php @@ -287,7 +287,7 @@ public function fail($exception): void ->format('Y-m-d\TH:i:s.u\Z')); $this->storedWorkflow->parents() - ->each(static function ($parentWorkflow) use ($exception) { + ->each(function ($parentWorkflow) use ($exception) { if ( $parentWorkflow->pivot->parent_index === StoredWorkflow::CONTINUE_PARENT_INDEX || $parentWorkflow->pivot->parent_index === StoredWorkflow::ACTIVE_WORKFLOW_INDEX @@ -324,7 +324,8 @@ public function fail($exception): void $parentWorkflow, $throwable, $parentWf->connection(), - $parentWf->queue() + $parentWf->queue(), + $this->storedWorkflow->class ); }); } diff --git a/tests/Fixtures/TestConsecutiveCaughtExceptionWorkflow.php b/tests/Fixtures/TestConsecutiveCaughtExceptionWorkflow.php new file mode 100644 index 00000000..8332876a --- /dev/null +++ b/tests/Fixtures/TestConsecutiveCaughtExceptionWorkflow.php @@ -0,0 +1,27 @@ +shouldContinue = true; + } + + public function execute() + { + try { + yield activity(TestSingleTryExceptionActivity::class, true); + } catch (Throwable) { + yield await(fn (): bool => $this->shouldContinue); + yield activity(TestSingleTryExceptionActivity::class, true); + } + + return 'handled'; + } +} diff --git a/tests/Unit/ExceptionTest.php b/tests/Unit/ExceptionTest.php index 74650224..0b802237 100644 --- a/tests/Unit/ExceptionTest.php +++ b/tests/Unit/ExceptionTest.php @@ -4,6 +4,10 @@ namespace Tests\Unit; +use Tests\Fixtures\TestConsecutiveCaughtExceptionWorkflow; +use Tests\Fixtures\TestParallelCaughtExceptionWorkflow; +use Tests\Fixtures\TestSignalAdvancedExceptionWorkflow; +use Tests\Fixtures\TestSingleTryExceptionActivity; use Tests\Fixtures\TestWorkflow; use Tests\TestCase; use Workflow\Exception; @@ -47,7 +51,7 @@ public function testExceptionWorkflowRunning(): void public function testSkipsWriteWhenSiblingExceptionLogExists(): void { - $workflow = WorkflowStub::load(WorkflowStub::make(TestWorkflow::class)->id()); + $workflow = WorkflowStub::load(WorkflowStub::make(TestParallelCaughtExceptionWorkflow::class)->id()); $storedWorkflow = StoredWorkflow::findOrFail($workflow->id()); $storedWorkflow->update([ 'arguments' => Serializer::serialize([]), @@ -71,16 +75,50 @@ public function testSkipsWriteWhenSiblingExceptionLogExists(): void 'class' => \Exception::class, 'message' => 'second child failed', 'code' => 0, - ]); + ], sourceClass: TestSingleTryExceptionActivity::class); $exception->handle(); $this->assertFalse($storedWorkflow->hasLogByIndex(1)); $this->assertSame(1, $storedWorkflow->logs()->count()); } + public function testWritesConsecutiveCaughtExceptionWithoutIntermediateLog(): void + { + $workflow = WorkflowStub::load(WorkflowStub::make(TestConsecutiveCaughtExceptionWorkflow::class)->id()); + $storedWorkflow = StoredWorkflow::findOrFail($workflow->id()); + $storedWorkflow->update([ + 'arguments' => Serializer::serialize([]), + 'status' => WorkflowRunningStatus::$name, + ]); + + $storedWorkflow->logs() + ->create([ + 'index' => 0, + 'now' => now() + ->toDateTimeString(), + 'class' => Exception::class, + 'result' => Serializer::serialize([ + 'class' => \RuntimeException::class, + 'message' => 'first failure', + 'code' => 0, + ]), + ]); + + $exception = new Exception(1, now()->toDateTimeString(), $storedWorkflow, [ + 'class' => \InvalidArgumentException::class, + 'message' => 'second failure', + 'code' => 0, + ], sourceClass: TestSingleTryExceptionActivity::class); + $exception->handle(); + + $this->assertTrue($storedWorkflow->hasLogByIndex(1)); + $this->assertSame(2, $storedWorkflow->logs()->count()); + $this->assertSame(Exception::class, $storedWorkflow->findLogByIndex(1)?->class); + } + public function testWritesLaterExceptionAfterWorkflowAdvances(): void { - $workflow = WorkflowStub::load(WorkflowStub::make(TestWorkflow::class)->id()); + $workflow = WorkflowStub::load(WorkflowStub::make(TestSignalAdvancedExceptionWorkflow::class)->id()); $storedWorkflow = StoredWorkflow::findOrFail($workflow->id()); $storedWorkflow->update([ 'arguments' => Serializer::serialize([]), @@ -113,7 +151,7 @@ public function testWritesLaterExceptionAfterWorkflowAdvances(): void 'class' => \InvalidArgumentException::class, 'message' => 'second failure', 'code' => 0, - ]); + ], sourceClass: TestSingleTryExceptionActivity::class); $exception->handle(); $this->assertTrue($storedWorkflow->hasLogByIndex(2)); From 5c97e9c496905e83212888ec2e6c51e8853e0ef5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Apr 2026 07:22:09 +0000 Subject: [PATCH 4/4] fix: isolate exception replay probe state Agent-Logs-Url: https://github.com/durable-workflow/workflow/sessions/3527603f-0ed5-4b5c-9bb8-bba1b504cb2d Co-authored-by: rmcdaniel <1130888+rmcdaniel@users.noreply.github.com> --- src/ActivityStub.php | 9 ++---- src/ChildWorkflowStub.php | 9 ++---- src/Exception.php | 7 ++--- src/Workflow.php | 16 ++-------- src/WorkflowStub.php | 41 +++++++++++++++++++++++++ tests/Feature/SagaChildWorkflowTest.php | 25 +++++++++++++++ tests/Unit/ExceptionTest.php | 25 ++++++++++----- 7 files changed, 93 insertions(+), 39 deletions(-) diff --git a/src/ActivityStub.php b/src/ActivityStub.php index 1c6ee6bb..a692ed9a 100644 --- a/src/ActivityStub.php +++ b/src/ActivityStub.php @@ -74,13 +74,8 @@ public static function make($activity, ...$arguments): PromiseInterface return resolve($result); } - if ($context->replaying && property_exists($context, 'probeIndex')) { - $context->probeMatched = $context->probeIndex === $context->index - && ( - ! property_exists($context, 'probeClass') - || $context->probeClass === null - || $context->probeClass === $activity - ); + if ($context->replaying && WorkflowStub::hasReplayProbe()) { + WorkflowStub::markReplayProbe($context->index, $activity); ++$context->index; WorkflowStub::setContext($context); $deferred = new Deferred(); diff --git a/src/ChildWorkflowStub.php b/src/ChildWorkflowStub.php index b3b2fbe8..419c345d 100644 --- a/src/ChildWorkflowStub.php +++ b/src/ChildWorkflowStub.php @@ -67,13 +67,8 @@ public static function make($workflow, ...$arguments): PromiseInterface return resolve($result); } - if ($context->replaying && property_exists($context, 'probeIndex')) { - $context->probeMatched = $context->probeIndex === $context->index - && ( - ! property_exists($context, 'probeClass') - || $context->probeClass === null - || $context->probeClass === $workflow - ); + if ($context->replaying && WorkflowStub::hasReplayProbe()) { + WorkflowStub::markReplayProbe($context->index, $workflow); ++$context->index; WorkflowStub::setContext($context); $deferred = new Deferred(); diff --git a/src/Exception.php b/src/Exception.php index d7ec9376..3c44d07d 100644 --- a/src/Exception.php +++ b/src/Exception.php @@ -88,22 +88,21 @@ private function isCurrentReplayFrontier(): bool $workflow->replaying = true; $previousContext = WorkflowStub::getContext(); + WorkflowStub::startReplayProbe($this->index, $this->sourceClass); WorkflowStub::setContext([ 'storedWorkflow' => $this->storedWorkflow, 'index' => 0, 'now' => $this->now, 'replaying' => true, - 'probeIndex' => $this->index, - 'probeClass' => $this->sourceClass, - 'probeMatched' => false, ]); try { $workflow->handle(); - return WorkflowStub::getContext()->probeMatched; + return WorkflowStub::replayProbeMatched(); } finally { + WorkflowStub::clearReplayProbe(); WorkflowStub::setContext($previousContext); } } diff --git a/src/Workflow.php b/src/Workflow.php index 994b4d18..20b80b6e 100644 --- a/src/Workflow.php +++ b/src/Workflow.php @@ -209,7 +209,7 @@ public function handle(): void $this->now = $log ? $log->now : Carbon::now(); } - $this->setContext([ + WorkflowStub::setContext([ 'storedWorkflow' => $this->storedWorkflow, 'index' => $this->index, 'now' => $this->now, @@ -229,7 +229,7 @@ public function handle(): void $this->now = $log ? $log->now : Carbon::now(); - $this->setContext([ + WorkflowStub::setContext([ 'storedWorkflow' => $this->storedWorkflow, 'index' => $this->index, 'now' => $this->now, @@ -309,16 +309,4 @@ public function handle(): void } } } - - private function setContext(array $context): void - { - $existingContext = WorkflowStub::getContext(); - - if (property_exists($existingContext, 'probeIndex')) { - $context['probeIndex'] = $existingContext->probeIndex; - $context['probeMatched'] = $existingContext->probeMatched ?? false; - } - - WorkflowStub::setContext($context); - } } diff --git a/src/WorkflowStub.php b/src/WorkflowStub.php index bdb344c4..caf0731b 100644 --- a/src/WorkflowStub.php +++ b/src/WorkflowStub.php @@ -41,6 +41,8 @@ final class WorkflowStub private static ?\stdClass $context = null; + private static ?array $replayProbe = null; + private static array $signalMethodCache = []; private static array $queryMethodCache = []; @@ -165,6 +167,45 @@ public static function setContext($context): void self::$context = (object) $context; } + public static function hasReplayProbe(): bool + { + return self::$replayProbe !== null; + } + + public static function startReplayProbe(int $index, ?string $class = null): void + { + self::$replayProbe = [ + 'index' => $index, + 'class' => $class, + 'matched' => false, + 'seen' => false, + ]; + } + + public static function markReplayProbe(int $index, ?string $class = null): void + { + if (self::$replayProbe === null || self::$replayProbe['seen'] === true) { + return; + } + + self::$replayProbe['seen'] = true; + self::$replayProbe['matched'] = self::$replayProbe['index'] === $index + && ( + self::$replayProbe['class'] === null + || self::$replayProbe['class'] === $class + ); + } + + public static function replayProbeMatched(): bool + { + return self::$replayProbe['matched'] ?? false; + } + + public static function clearReplayProbe(): void + { + self::$replayProbe = null; + } + public static function now() { return self::getContext()->now; diff --git a/tests/Feature/SagaChildWorkflowTest.php b/tests/Feature/SagaChildWorkflowTest.php index b99255c6..84ac540b 100644 --- a/tests/Feature/SagaChildWorkflowTest.php +++ b/tests/Feature/SagaChildWorkflowTest.php @@ -4,9 +4,14 @@ namespace Tests\Feature; +use Tests\Fixtures\TestActivity; +use Tests\Fixtures\TestChildExceptionThrowingWorkflow; use Tests\Fixtures\TestSagaChildWorkflow; use Tests\Fixtures\TestSagaSingleChildWorkflow; +use Tests\Fixtures\TestUndoActivity; use Tests\TestCase; +use Workflow\Exception; +use Workflow\Models\StoredWorkflow; use Workflow\States\WorkflowCompletedStatus; use Workflow\WorkflowStub; @@ -34,5 +39,25 @@ public function testParallelChildExceptionsTriggersCompensation(): void $this->assertSame(WorkflowCompletedStatus::class, $workflow->status()); $this->assertSame('compensated', $workflow->output()); + + $storedWorkflow = StoredWorkflow::findOrFail($workflow->id()); + + $this->assertEqualsCanonicalizing([ + TestActivity::class, + TestUndoActivity::class, + Exception::class, + ], $storedWorkflow->logs() + ->pluck('class') + ->toArray()); + + $childLogs = $storedWorkflow->children() + ->with('logs') + ->get() + ->flatMap(static fn (StoredWorkflow $childWorkflow) => $childWorkflow->logs->pluck('class')) + ->values() + ->toArray(); + + $this->assertNotContains(TestUndoActivity::class, $childLogs); + $this->assertContains(TestChildExceptionThrowingWorkflow::class, $childLogs); } } diff --git a/tests/Unit/ExceptionTest.php b/tests/Unit/ExceptionTest.php index 0b802237..0ec8b6e1 100644 --- a/tests/Unit/ExceptionTest.php +++ b/tests/Unit/ExceptionTest.php @@ -4,8 +4,10 @@ namespace Tests\Unit; +use Tests\Fixtures\TestActivity; +use Tests\Fixtures\TestChildExceptionThrowingWorkflow; use Tests\Fixtures\TestConsecutiveCaughtExceptionWorkflow; -use Tests\Fixtures\TestParallelCaughtExceptionWorkflow; +use Tests\Fixtures\TestSagaChildWorkflow; use Tests\Fixtures\TestSignalAdvancedExceptionWorkflow; use Tests\Fixtures\TestSingleTryExceptionActivity; use Tests\Fixtures\TestWorkflow; @@ -51,7 +53,7 @@ public function testExceptionWorkflowRunning(): void public function testSkipsWriteWhenSiblingExceptionLogExists(): void { - $workflow = WorkflowStub::load(WorkflowStub::make(TestParallelCaughtExceptionWorkflow::class)->id()); + $workflow = WorkflowStub::load(WorkflowStub::make(TestSagaChildWorkflow::class)->id()); $storedWorkflow = StoredWorkflow::findOrFail($workflow->id()); $storedWorkflow->update([ 'arguments' => Serializer::serialize([]), @@ -61,25 +63,34 @@ public function testSkipsWriteWhenSiblingExceptionLogExists(): void $storedWorkflow->logs() ->create([ 'index' => 0, + 'now' => now() + ->toDateTimeString(), + 'class' => TestActivity::class, + 'result' => Serializer::serialize('activity'), + ]); + + $storedWorkflow->logs() + ->create([ + 'index' => 1, 'now' => now() ->toDateTimeString(), 'class' => Exception::class, 'result' => Serializer::serialize([ 'class' => \Exception::class, - 'message' => 'first child failed', + 'message' => 'first parallel child failed', 'code' => 0, ]), ]); - $exception = new Exception(1, now()->toDateTimeString(), $storedWorkflow, [ + $exception = new Exception(2, now()->toDateTimeString(), $storedWorkflow, [ 'class' => \Exception::class, 'message' => 'second child failed', 'code' => 0, - ], sourceClass: TestSingleTryExceptionActivity::class); + ], sourceClass: TestChildExceptionThrowingWorkflow::class); $exception->handle(); - $this->assertFalse($storedWorkflow->hasLogByIndex(1)); - $this->assertSame(1, $storedWorkflow->logs()->count()); + $this->assertFalse($storedWorkflow->hasLogByIndex(2)); + $this->assertSame(2, $storedWorkflow->logs()->count()); } public function testWritesConsecutiveCaughtExceptionWithoutIntermediateLog(): void