|
23 | 23 | use Workflow\V2\Models\WorkflowRun; |
24 | 24 | use Workflow\V2\Models\WorkflowTask; |
25 | 25 | use Workflow\V2\Support\ActivityOutcomeRecorder; |
| 26 | +use Workflow\V2\Support\FailureFactory; |
| 27 | +use Workflow\V2\Support\FailureSnapshots; |
26 | 28 |
|
27 | 29 | /** |
28 | 30 | * TD-089 regression: activity exception rows must be encoded with the |
@@ -186,6 +188,73 @@ public function testCompletionHistoryStampsWorkerResultCodec(): void |
186 | 188 | ); |
187 | 189 | } |
188 | 190 |
|
| 191 | + public function testFailureDetailsEnvelopePreservesWorkerCodecInTypedHistory(): void |
| 192 | + { |
| 193 | + config()->set('workflows.serializer', 'avro'); |
| 194 | + |
| 195 | + [$run, $execution, $task, $attempt] = $this->scaffoldLeasedAttempt( |
| 196 | + pinnedCodec: 'json', |
| 197 | + maxAttempts: 2, |
| 198 | + instanceId: 'td090-failure-details-json', |
| 199 | + ); |
| 200 | + |
| 201 | + $detailsBlob = Serializer::serializeWithCodec('json', ['retry_after' => 30]); |
| 202 | + |
| 203 | + $outcome = ActivityOutcomeRecorder::record( |
| 204 | + taskId: $task->id, |
| 205 | + attemptId: $attempt->id, |
| 206 | + attemptCount: 1, |
| 207 | + result: null, |
| 208 | + throwable: FailureFactory::restore([ |
| 209 | + 'class' => RuntimeException::class, |
| 210 | + 'type' => 'TimeoutException', |
| 211 | + 'message' => 'external timeout', |
| 212 | + 'non_retryable' => true, |
| 213 | + 'details' => $detailsBlob, |
| 214 | + ]), |
| 215 | + maxAttempts: 2, |
| 216 | + backoffSeconds: 0, |
| 217 | + codec: 'json', |
| 218 | + ); |
| 219 | + |
| 220 | + $this->assertTrue($outcome['recorded']); |
| 221 | + |
| 222 | + $execution->refresh(); |
| 223 | + $decodedException = Serializer::unserializeWithCodec('json', (string) $execution->exception); |
| 224 | + |
| 225 | + $this->assertSame($detailsBlob, $decodedException['details'] ?? null); |
| 226 | + $this->assertSame('json', $decodedException['details_payload_codec'] ?? null); |
| 227 | + $this->assertTrue($decodedException['non_retryable'] ?? false); |
| 228 | + |
| 229 | + /** @var WorkflowHistoryEvent $failed */ |
| 230 | + $failed = WorkflowHistoryEvent::query() |
| 231 | + ->where('workflow_run_id', $run->id) |
| 232 | + ->where('event_type', HistoryEventType::ActivityFailed->value) |
| 233 | + ->firstOrFail(); |
| 234 | + |
| 235 | + $this->assertSame(RuntimeException::class, $failed->payload['exception_class'] ?? null); |
| 236 | + $this->assertSame('TimeoutException', $failed->payload['exception_type'] ?? null); |
| 237 | + $this->assertTrue($failed->payload['non_retryable'] ?? false); |
| 238 | + $this->assertSame($detailsBlob, $failed->payload['exception']['details'] ?? null); |
| 239 | + $this->assertSame('json', $failed->payload['exception']['details_payload_codec'] ?? null); |
| 240 | + $this->assertTrue($failed->payload['exception']['non_retryable'] ?? false); |
| 241 | + |
| 242 | + $this->assertFalse( |
| 243 | + WorkflowHistoryEvent::query() |
| 244 | + ->where('workflow_run_id', $run->id) |
| 245 | + ->where('event_type', HistoryEventType::ActivityRetryScheduled->value) |
| 246 | + ->exists(), |
| 247 | + ); |
| 248 | + |
| 249 | + $snapshot = FailureSnapshots::forRun($run->fresh(['historyEvents', 'failures']))[0] ?? null; |
| 250 | + |
| 251 | + $this->assertIsArray($snapshot); |
| 252 | + $this->assertTrue($snapshot['non_retryable'] ?? false); |
| 253 | + $this->assertSame($detailsBlob, $snapshot['exception_payload']['details'] ?? null); |
| 254 | + $this->assertSame('json', $snapshot['exception_payload']['details_payload_codec'] ?? null); |
| 255 | + $this->assertTrue($snapshot['exception_payload']['non_retryable'] ?? false); |
| 256 | + } |
| 257 | + |
189 | 258 | /** |
190 | 259 | * @return array{0: WorkflowRun, 1: ActivityExecution, 2: WorkflowTask, 3: ActivityAttempt} |
191 | 260 | */ |
|
0 commit comments