Skip to content

Commit 789f3b6

Browse files
authored
Merge pull request #917 from durable-workflow/fix/898-activity-entry-fixture-scope
Keep activity entry-safety tests scoped to their original run
2 parents e943469 + 463b86a commit 789f3b6

2 files changed

Lines changed: 91 additions & 28 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,14 @@
11
# Changelog
22

3-
## Unreleased
4-
5-
- Bound derived timeline summaries by UTF-8 bytes so a large accepted activity
6-
failure can be recorded without overflowing MySQL TEXT storage. Preserve the
7-
full failure evidence and callback-authority recovery notice.
8-
93
## 2.12.0 - 2026-10-23
104

115
- Preserve UTC instants and microseconds in datetime search attributes and
126
embedded visibility predicates, including values with non-UTC offsets.
137
- Add `workflow:v2:repair-search-datetimes` to preview or explicitly repair
148
selected datetime projections from matching retained, typed history.
9+
- Bound derived timeline summaries by UTF-8 bytes so a large accepted activity
10+
failure can be recorded without overflowing MySQL TEXT storage. Preserve the
11+
full failure evidence and callback-authority recovery notice.
1512

1613
## 2.11.4 - 2026-10-23
1714

‎tests/Feature/V2/V2ActivityEntryArgumentsTest.php‎

Lines changed: 88 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use Illuminate\Support\Facades\Queue;
99
use Orchestra\Testbench\TestCase;
1010
use PHPUnit\Framework\Attributes\DataProvider;
11+
use Tests\Fixtures\V2\TestGreetingWorkflow;
1112
use Workflow\Providers\WorkflowServiceProvider;
1213
use Workflow\V2\Activity;
1314
use Workflow\V2\Enums\ActivityStatus;
@@ -34,6 +35,36 @@ protected function setUp(): void
3435
ActivityEntryWorkflow::$local = false;
3536
}
3637

38+
public function testEntrySafetyChecksDoNotClaimAnUnrelatedRunsReadyTask(): void
39+
{
40+
$foreign = WorkflowStub::make(TestGreetingWorkflow::class, 'unrelated-entry-control');
41+
$foreign->start('Other');
42+
$foreignTask = WorkflowTask::query()->where('workflow_run_id', $foreign->runId())->sole();
43+
$foreignBefore = $foreignTask->getAttributes();
44+
$foreignHistoryCount = WorkflowHistoryEvent::query()
45+
->where('workflow_run_id', $foreign->runId())
46+
->count();
47+
48+
[$stub, $execution, $task, $arguments] = $this->scheduled(ActivityEntryDto::class, [7]);
49+
$this->app->call([new RunActivityTask($task->id), 'handle']);
50+
51+
self::assertNotSame($foreign->runId(), $stub->runId());
52+
self::assertFalse(ActivityEntryDto::$entered);
53+
self::assertSame($arguments, $execution->fresh()->getRawOriginal('arguments'));
54+
self::assertSame(ActivityStatus::Failed, $execution->fresh()->status);
55+
self::assertSame(
56+
ActivityEntryArgumentsMismatchException::class,
57+
$this->history($stub->runId(), 'ActivityFailed')
58+
->payload['exception_class'],
59+
);
60+
$this->runWorkflowTask($stub->runId());
61+
self::assertTrue($stub->refresh()->failed());
62+
self::assertSame($foreignBefore, $foreignTask->fresh()->getAttributes());
63+
self::assertSame($foreignHistoryCount, WorkflowHistoryEvent::query()
64+
->where('workflow_run_id', $foreign->runId())
65+
->count());
66+
}
67+
3768
public function testInjectedDtoCannotReplaceTheRecordedBusinessArgument(): void
3869
{
3970
[$stub, $execution, $task, $arguments] = $this->scheduled(ActivityEntryDto::class, [7]);
@@ -48,10 +79,10 @@ public function testInjectedDtoCannotReplaceTheRecordedBusinessArgument(): void
4879
self::assertSame(ActivityEntryArgumentsMismatchException::class, $event->payload['exception_class']);
4980
self::assertStringContainsString('recorded_argument_would_be_discarded', $event->payload['message']);
5081

51-
$this->runWorkflowTask();
82+
$this->runWorkflowTask($stub->runId());
5283
self::assertTrue($stub->refresh()->failed());
5384
self::assertNull($stub->output());
54-
self::assertSame(0, WorkflowTask::query()->sum('repair_count'));
85+
self::assertSame(0, WorkflowTask::query()->where('workflow_run_id', $stub->runId())->sum('repair_count'));
5586
}
5687

5788
public function testAuthoredRetryUsesTheSameOriginalInputAfterCompatibleCodeIsRestored(): void
@@ -71,20 +102,35 @@ public function testAuthoredRetryUsesTheSameOriginalInputAfterCompatibleCodeIsRe
71102
// A new deployment can supply a compatible implementation through the
72103
// normal Laravel binding without rewriting durable class/input fields.
73104
$this->app->bind(ActivityEntryRetryDto::class, static fn (): Activity => new ActivityEntryScalar());
74-
$retryTask = WorkflowTask::query()->where('task_type', TaskType::Activity->value)
105+
$retryTask = WorkflowTask::query()->where('workflow_run_id', $stub->runId())->where(
106+
'task_type',
107+
TaskType::Activity->value
108+
)
75109
->where('status', TaskStatus::Ready->value)->sole();
76110
$this->app->call([new RunActivityTask($retryTask->id), 'handle']);
77-
$this->runWorkflowTask();
111+
$this->runWorkflowTask($stub->runId());
78112

79113
self::assertTrue($stub->refresh()->completed());
80114
self::assertSame(7, $stub->output());
81115
self::assertSame($originalRunId, $stub->runId());
82116
self::assertSame($originalExecutionId, $execution->fresh()->id);
83117
self::assertSame($arguments, $execution->fresh()->getRawOriginal('arguments'));
84118
self::assertSame(2, $execution->fresh()->attempt_count);
85-
self::assertSame(1, WorkflowHistoryEvent::query()->where('event_type', 'ActivityCompleted')->count());
86-
self::assertSame(0, WorkflowHistoryEvent::query()->where('event_type', 'ActivityFailed')->count());
87-
self::assertSame(0, WorkflowTask::query()->sum('repair_count'));
119+
self::assertSame(
120+
1,
121+
WorkflowHistoryEvent::query()->where('workflow_run_id', $stub->runId())->where(
122+
'event_type',
123+
'ActivityCompleted'
124+
)->count()
125+
);
126+
self::assertSame(
127+
0,
128+
WorkflowHistoryEvent::query()->where('workflow_run_id', $stub->runId())->where(
129+
'event_type',
130+
'ActivityFailed'
131+
)->count()
132+
);
133+
self::assertSame(0, WorkflowTask::query()->where('workflow_run_id', $stub->runId())->sum('repair_count'));
88134
}
89135

90136
#[DataProvider('compatibleEntries')]
@@ -95,13 +141,13 @@ public function testLegitimateDependencyInjectionAndDefaultsKeepRecordedValues(
95141
): void {
96142
[$stub, $execution, $task, $storedArguments] = $this->scheduled($activity, $arguments);
97143
$this->app->call([new RunActivityTask($task->id), 'handle']);
98-
$this->runWorkflowTask();
144+
$this->runWorkflowTask($stub->runId());
99145

100146
self::assertTrue($stub->refresh()->completed());
101147
self::assertSame($expected, $stub->output());
102148
self::assertSame($storedArguments, $execution->fresh()->getRawOriginal('arguments'));
103149
self::assertSame(1, $execution->fresh()->attempt_count);
104-
self::assertSame(0, WorkflowTask::query()->sum('repair_count'));
150+
self::assertSame(0, WorkflowTask::query()->where('workflow_run_id', $stub->runId())->sum('repair_count'));
105151
}
106152

107153
public static function compatibleEntries(): iterable
@@ -118,16 +164,22 @@ public function testLocalActivityCannotDiscardRecordedInputDuringDependencyInjec
118164
ActivityEntryWorkflow::$arguments = [7];
119165
$stub = WorkflowStub::make(ActivityEntryWorkflow::class);
120166
$stub->start();
121-
$this->runWorkflowTask();
167+
$this->runWorkflowTask($stub->runId());
122168

123169
self::assertFalse(ActivityEntryDto::$entered, 'The incompatible local activity body ran.');
124-
$execution = ActivityExecution::query()->sole();
170+
$execution = ActivityExecution::query()->where('workflow_run_id', $stub->runId())->sole();
125171
self::assertSame([7], $execution->activityArguments());
126172
self::assertSame(ActivityStatus::Failed, $execution->status);
127173
self::assertSame(1, $execution->attempt_count);
128174
$event = $this->history($stub->runId(), 'ActivityFailed');
129175
self::assertSame(ActivityEntryArgumentsMismatchException::class, $event->payload['exception_class']);
130-
self::assertSame(0, WorkflowTask::query()->where('task_type', TaskType::Activity->value)->count());
176+
self::assertSame(
177+
0,
178+
WorkflowTask::query()->where('workflow_run_id', $stub->runId())->where(
179+
'task_type',
180+
TaskType::Activity->value
181+
)->count()
182+
);
131183
self::assertTrue($stub->refresh()->failed());
132184
}
133185

@@ -139,15 +191,18 @@ public function testDependencyResolutionFailureHasAnActivityOutcome(bool $local)
139191
ActivityEntryWorkflow::$arguments = [7];
140192
$stub = WorkflowStub::make(ActivityEntryWorkflow::class);
141193
$stub->start();
142-
$this->runWorkflowTask();
194+
$this->runWorkflowTask($stub->runId());
143195
if (! $local) {
144-
$task = WorkflowTask::query()->where('task_type', TaskType::Activity->value)->sole();
196+
$task = WorkflowTask::query()->where('workflow_run_id', $stub->runId())->where(
197+
'task_type',
198+
TaskType::Activity->value
199+
)->sole();
145200
$this->app->call([new RunActivityTask($task->id), 'handle']);
146-
$this->runWorkflowTask();
201+
$this->runWorkflowTask($stub->runId());
147202
}
148203

149204
self::assertFalse(ActivityEntryDto::$entered);
150-
$execution = ActivityExecution::query()->sole();
205+
$execution = ActivityExecution::query()->where('workflow_run_id', $stub->runId())->sole();
151206
self::assertSame(ActivityStatus::Failed, $execution->status);
152207
self::assertSame([7], $execution->activityArguments());
153208
self::assertSame(1, $execution->attempt_count);
@@ -157,7 +212,13 @@ public function testDependencyResolutionFailureHasAnActivityOutcome(bool $local)
157212
$event->payload['exception_class']
158213
);
159214
self::assertTrue($stub->refresh()->failed());
160-
self::assertSame(0, WorkflowTask::query()->where('status', TaskStatus::Leased->value)->count());
215+
self::assertSame(
216+
0,
217+
WorkflowTask::query()->where('workflow_run_id', $stub->runId())->where(
218+
'status',
219+
TaskStatus::Leased->value
220+
)->count()
221+
);
161222
}
162223

163224
public static function executionModes(): iterable
@@ -195,16 +256,21 @@ private function scheduled(string $activity, array $arguments): array
195256
ActivityEntryWorkflow::$arguments = $arguments;
196257
$stub = WorkflowStub::make(ActivityEntryWorkflow::class);
197258
$stub->start();
198-
$this->runWorkflowTask();
199-
$execution = ActivityExecution::query()->sole();
200-
$task = WorkflowTask::query()->where('task_type', TaskType::Activity->value)->sole();
259+
$this->runWorkflowTask($stub->runId());
260+
$execution = ActivityExecution::query()->where('workflow_run_id', $stub->runId())->sole();
261+
$task = WorkflowTask::query()->where('workflow_run_id', $stub->runId())->where(
262+
'task_type',
263+
TaskType::Activity->value
264+
)->sole();
201265

202266
return [$stub, $execution, $task, $execution->getRawOriginal('arguments')];
203267
}
204268

205-
private function runWorkflowTask(): void
269+
private function runWorkflowTask(string $runId): void
206270
{
207-
$task = WorkflowTask::query()->where('task_type', TaskType::Workflow->value)
271+
$task = WorkflowTask::query()
272+
->where('workflow_run_id', $runId)
273+
->where('task_type', TaskType::Workflow->value)
208274
->where('status', TaskStatus::Ready->value)->sole();
209275
$this->app->call([new RunWorkflowTask($task->id), 'handle']);
210276
}

0 commit comments

Comments
 (0)