|
20 | 20 | use Workflow\V2\Models\WorkflowLink; |
21 | 21 | use Workflow\V2\Models\WorkflowRun; |
22 | 22 | use Workflow\V2\Models\WorkflowTask; |
| 23 | +use Workflow\V2\Support\ParentClosePolicyEnforcer; |
23 | 24 | use Workflow\V2\Support\RunLineageView; |
24 | 25 | use Workflow\V2\Support\WorkflowExecutor; |
25 | 26 | use Workflow\V2\WorkflowStub; |
@@ -440,6 +441,151 @@ public function testTerminatePolicyTerminatesChildWhenParentTimesOut(): void |
440 | 441 | $this->assertSame('terminate', $appliedEvent->payload['policy']); |
441 | 442 | } |
442 | 443 |
|
| 444 | + public function testEnforcerCancelsOpenChildrenOfFailedParentRun(): void |
| 445 | + { |
| 446 | + // The executor's failRun path marks a run Failed and then calls |
| 447 | + // ParentClosePolicyEnforcer::enforce. Drive the enforcer directly |
| 448 | + // against a Failed parent to prove the failure exit applies policy |
| 449 | + // to open children (failed path is otherwise only reachable through |
| 450 | + // a fiber terminal-throw, which cannot naturally leave children open |
| 451 | + // in straight-line v2). |
| 452 | + $workflow = WorkflowStub::make(TestParentWithClosePolicyWorkflow::class, 'enforce-on-failure'); |
| 453 | + $workflow->start('request_cancel'); |
| 454 | + |
| 455 | + $this->drainReadyTasks(); |
| 456 | + |
| 457 | + $this->assertSame('waiting', $workflow->refresh()->status()); |
| 458 | + |
| 459 | + $link = WorkflowLink::query() |
| 460 | + ->where('parent_workflow_instance_id', 'enforce-on-failure') |
| 461 | + ->where('link_type', 'child_workflow') |
| 462 | + ->sole(); |
| 463 | + |
| 464 | + $childInstanceId = $link->child_workflow_instance_id; |
| 465 | + $childStub = WorkflowStub::load($childInstanceId); |
| 466 | + $this->assertContains($childStub->status(), ['waiting', 'running', 'pending']); |
| 467 | + |
| 468 | + /** @var WorkflowRun $parentRun */ |
| 469 | + $parentRun = WorkflowRun::query() |
| 470 | + ->where('workflow_instance_id', 'enforce-on-failure') |
| 471 | + ->firstOrFail(); |
| 472 | + |
| 473 | + // Mimic the terminal state the executor persists in failRun before |
| 474 | + // calling the enforcer. |
| 475 | + $parentRun->forceFill([ |
| 476 | + 'status' => RunStatus::Failed, |
| 477 | + 'closed_reason' => 'failed', |
| 478 | + 'closed_at' => Carbon::now(), |
| 479 | + ])->save(); |
| 480 | + |
| 481 | + $applied = ParentClosePolicyEnforcer::enforce($parentRun->fresh()); |
| 482 | + |
| 483 | + $this->assertSame([$childInstanceId], $applied); |
| 484 | + |
| 485 | + $childStub->refresh(); |
| 486 | + $this->assertSame('cancelled', $childStub->status()); |
| 487 | + |
| 488 | + $appliedEvent = $parentRun->historyEvents() |
| 489 | + ->where('event_type', HistoryEventType::ParentClosePolicyApplied->value) |
| 490 | + ->first(); |
| 491 | + |
| 492 | + $this->assertNotNull($appliedEvent, 'Parent-close policy should be enforced when parent fails.'); |
| 493 | + $this->assertSame('request_cancel', $appliedEvent->payload['policy']); |
| 494 | + $this->assertSame($childInstanceId, $appliedEvent->payload['child_instance_id']); |
| 495 | + } |
| 496 | + |
| 497 | + public function testEnforcerTerminatesOpenChildrenOfFailedParentRun(): void |
| 498 | + { |
| 499 | + $workflow = WorkflowStub::make(TestParentWithClosePolicyWorkflow::class, 'enforce-terminate-on-failure'); |
| 500 | + $workflow->start('terminate'); |
| 501 | + |
| 502 | + $this->drainReadyTasks(); |
| 503 | + |
| 504 | + $this->assertSame('waiting', $workflow->refresh()->status()); |
| 505 | + |
| 506 | + $link = WorkflowLink::query() |
| 507 | + ->where('parent_workflow_instance_id', 'enforce-terminate-on-failure') |
| 508 | + ->where('link_type', 'child_workflow') |
| 509 | + ->sole(); |
| 510 | + |
| 511 | + $childInstanceId = $link->child_workflow_instance_id; |
| 512 | + $childStub = WorkflowStub::load($childInstanceId); |
| 513 | + $this->assertContains($childStub->status(), ['waiting', 'running', 'pending']); |
| 514 | + |
| 515 | + /** @var WorkflowRun $parentRun */ |
| 516 | + $parentRun = WorkflowRun::query() |
| 517 | + ->where('workflow_instance_id', 'enforce-terminate-on-failure') |
| 518 | + ->firstOrFail(); |
| 519 | + |
| 520 | + $parentRun->forceFill([ |
| 521 | + 'status' => RunStatus::Failed, |
| 522 | + 'closed_reason' => 'failed', |
| 523 | + 'closed_at' => Carbon::now(), |
| 524 | + ])->save(); |
| 525 | + |
| 526 | + $applied = ParentClosePolicyEnforcer::enforce($parentRun->fresh()); |
| 527 | + |
| 528 | + $this->assertSame([$childInstanceId], $applied); |
| 529 | + |
| 530 | + $childStub->refresh(); |
| 531 | + $this->assertSame('terminated', $childStub->status()); |
| 532 | + |
| 533 | + $appliedEvent = $parentRun->historyEvents() |
| 534 | + ->where('event_type', HistoryEventType::ParentClosePolicyApplied->value) |
| 535 | + ->first(); |
| 536 | + |
| 537 | + $this->assertNotNull($appliedEvent); |
| 538 | + $this->assertSame('terminate', $appliedEvent->payload['policy']); |
| 539 | + } |
| 540 | + |
| 541 | + public function testEnforcerLeavesChildrenRunningForCompletedParentWithAbandonPolicy(): void |
| 542 | + { |
| 543 | + // The executor also calls the enforcer from its WorkflowCompleted |
| 544 | + // path; a straight-line parent cannot naturally complete with open |
| 545 | + // children, so drive the enforcer against a Completed parent |
| 546 | + // directly. Abandon must stay a no-op with no applied history event. |
| 547 | + $workflow = WorkflowStub::make(TestParentWithClosePolicyWorkflow::class, 'enforce-abandon-on-completion'); |
| 548 | + $workflow->start('abandon'); |
| 549 | + |
| 550 | + $this->drainReadyTasks(); |
| 551 | + |
| 552 | + $this->assertSame('waiting', $workflow->refresh()->status()); |
| 553 | + |
| 554 | + $link = WorkflowLink::query() |
| 555 | + ->where('parent_workflow_instance_id', 'enforce-abandon-on-completion') |
| 556 | + ->where('link_type', 'child_workflow') |
| 557 | + ->sole(); |
| 558 | + |
| 559 | + $childInstanceId = $link->child_workflow_instance_id; |
| 560 | + $childStub = WorkflowStub::load($childInstanceId); |
| 561 | + $childStatusBefore = $childStub->status(); |
| 562 | + $this->assertContains($childStatusBefore, ['waiting', 'running', 'pending']); |
| 563 | + |
| 564 | + /** @var WorkflowRun $parentRun */ |
| 565 | + $parentRun = WorkflowRun::query() |
| 566 | + ->where('workflow_instance_id', 'enforce-abandon-on-completion') |
| 567 | + ->firstOrFail(); |
| 568 | + |
| 569 | + $parentRun->forceFill([ |
| 570 | + 'status' => RunStatus::Completed, |
| 571 | + 'closed_reason' => 'completed', |
| 572 | + 'closed_at' => Carbon::now(), |
| 573 | + ])->save(); |
| 574 | + |
| 575 | + $applied = ParentClosePolicyEnforcer::enforce($parentRun->fresh()); |
| 576 | + |
| 577 | + $this->assertSame([], $applied); |
| 578 | + |
| 579 | + $childStub->refresh(); |
| 580 | + $this->assertContains($childStub->status(), ['waiting', 'running', 'pending']); |
| 581 | + |
| 582 | + $appliedEvent = $parentRun->historyEvents() |
| 583 | + ->where('event_type', HistoryEventType::ParentClosePolicyApplied->value) |
| 584 | + ->first(); |
| 585 | + |
| 586 | + $this->assertNull($appliedEvent, 'Abandon policy should record no applied history event.'); |
| 587 | + } |
| 588 | + |
443 | 589 | public function testRunLineageViewSurfacesAppliedRequestCancelOutcomeOnChildEntry(): void |
444 | 590 | { |
445 | 591 | $workflow = WorkflowStub::make(TestParentWithClosePolicyWorkflow::class, 'lineage-applied-cancel'); |
|
0 commit comments