Skip to content

Commit 8bda213

Browse files
Cover parent-close policy enforcement on failed and completed parents
Add coverage that drives ParentClosePolicyEnforcer::enforce directly against terminal parents that straight-line child() calls cannot reach naturally. The existing feature suite proves enforcement on terminate, cancel, and timeout closures; this adds the remaining failure and completion exits the executor already calls into. testEnforcerCancelsOpenChildrenOfFailedParentRun and testEnforcerTerminatesOpenChildrenOfFailedParentRun exercise a Failed parent with an open child under request_cancel and terminate policies, mirroring the failRun exit in WorkflowExecutor. testEnforcerLeavesChildrenRunningForCompletedParentWithAbandonPolicy pins the defensive Completed-parent path: abandon policy applies no command and records no ParentClosePolicyApplied history event.
1 parent 54216f3 commit 8bda213

1 file changed

Lines changed: 146 additions & 0 deletions

File tree

tests/Feature/V2/V2ParentClosePolicyTest.php

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use Workflow\V2\Models\WorkflowLink;
2121
use Workflow\V2\Models\WorkflowRun;
2222
use Workflow\V2\Models\WorkflowTask;
23+
use Workflow\V2\Support\ParentClosePolicyEnforcer;
2324
use Workflow\V2\Support\RunLineageView;
2425
use Workflow\V2\Support\WorkflowExecutor;
2526
use Workflow\V2\WorkflowStub;
@@ -440,6 +441,151 @@ public function testTerminatePolicyTerminatesChildWhenParentTimesOut(): void
440441
$this->assertSame('terminate', $appliedEvent->payload['policy']);
441442
}
442443

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+
443589
public function testRunLineageViewSurfacesAppliedRequestCancelOutcomeOnChildEntry(): void
444590
{
445591
$workflow = WorkflowStub::make(TestParentWithClosePolicyWorkflow::class, 'lineage-applied-cancel');

0 commit comments

Comments
 (0)