From f71b9988e3c34c46f856948c49b96349c778a573 Mon Sep 17 00:00:00 2001 From: Dan Lemon Date: Tue, 28 Apr 2026 12:48:31 +0300 Subject: [PATCH 1/7] chore: skip steps --- .../BaseJob.php | 82 ++++++++++++++++++- .../Claim/ClaimJob.php | 6 ++ .../Create/CreateJob.php | 6 ++ .../Create/PostCreateJob.php | 6 ++ .../Create/PreCreateJob.php | 6 ++ .../Deploy/DeployJob.php | 6 ++ .../Deploy/PostDeployJob.php | 6 ++ .../Deploy/PreDeployJob.php | 6 ++ .../New/ProcessNewJob.php | 6 ++ .../Remove/PostRemoveJob.php | 6 ++ .../Remove/PreRemoveJob.php | 6 ++ .../Remove/RemoveJob.php | 6 ++ tests/Feature/Jobs/StaleLifecycleJobTest.php | 47 +++++++++++ 13 files changed, 194 insertions(+), 1 deletion(-) create mode 100644 tests/Feature/Jobs/StaleLifecycleJobTest.php diff --git a/app/Jobs/ProcessPolydockAppInstanceJobs/BaseJob.php b/app/Jobs/ProcessPolydockAppInstanceJobs/BaseJob.php index 79e29f7f..1d5b65df 100644 --- a/app/Jobs/ProcessPolydockAppInstanceJobs/BaseJob.php +++ b/app/Jobs/ProcessPolydockAppInstanceJobs/BaseJob.php @@ -3,6 +3,7 @@ namespace App\Jobs\ProcessPolydockAppInstanceJobs; use App\Models\PolydockAppInstance; +use FreedomtechHosting\PolydockApp\Enums\PolydockAppInstanceStatus; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; @@ -18,6 +19,8 @@ abstract class BaseJob implements ShouldQueue use Queueable; use SerializesModels; + protected const OVERLAP_LOCK_SECONDS = 120; + protected PolydockAppInstance $appInstance; /** @@ -81,12 +84,89 @@ public function middleware() return [ (new WithoutOverlapping($uniqueId)) - ->expireAfter(5) // 5 seconds + ->expireAfter(self::OVERLAP_LOCK_SECONDS) ->shared() // Use shared lock across different queues ->dontRelease(), ]; } + protected function shouldSkipBecauseStatusAdvanced(PolydockAppInstanceStatus $expectedStatus): bool + { + if (! isset($this->appInstance)) { + return false; + } + + $currentStatus = $this->appInstance->status; + + if ($currentStatus === $expectedStatus) { + return false; + } + + if (! $this->isKnownStatusProgression($expectedStatus, $currentStatus)) { + return false; + } + + Log::info('Skipping stale lifecycle job because app instance already advanced', [ + 'job_type' => class_basename(static::class), + 'app_instance_id' => $this->appInstance->id, + 'expected_status' => $expectedStatus->value, + 'current_status' => $currentStatus->value, + ]); + + return true; + } + + private function isKnownStatusProgression(PolydockAppInstanceStatus $expectedStatus, PolydockAppInstanceStatus $currentStatus): bool + { + $statusOrder = [ + PolydockAppInstanceStatus::NEW, + PolydockAppInstanceStatus::PENDING_PRE_CREATE, + PolydockAppInstanceStatus::PRE_CREATE_RUNNING, + PolydockAppInstanceStatus::PRE_CREATE_COMPLETED, + PolydockAppInstanceStatus::PENDING_CREATE, + PolydockAppInstanceStatus::CREATE_RUNNING, + PolydockAppInstanceStatus::CREATE_COMPLETED, + PolydockAppInstanceStatus::PENDING_POST_CREATE, + PolydockAppInstanceStatus::POST_CREATE_RUNNING, + PolydockAppInstanceStatus::POST_CREATE_COMPLETED, + PolydockAppInstanceStatus::PENDING_PRE_DEPLOY, + PolydockAppInstanceStatus::PRE_DEPLOY_RUNNING, + PolydockAppInstanceStatus::PRE_DEPLOY_COMPLETED, + PolydockAppInstanceStatus::PENDING_DEPLOY, + PolydockAppInstanceStatus::DEPLOY_RUNNING, + PolydockAppInstanceStatus::DEPLOY_COMPLETED, + PolydockAppInstanceStatus::PENDING_POST_DEPLOY, + PolydockAppInstanceStatus::POST_DEPLOY_RUNNING, + PolydockAppInstanceStatus::POST_DEPLOY_COMPLETED, + PolydockAppInstanceStatus::PENDING_POLYDOCK_CLAIM, + PolydockAppInstanceStatus::POLYDOCK_CLAIM_RUNNING, + PolydockAppInstanceStatus::POLYDOCK_CLAIM_COMPLETED, + PolydockAppInstanceStatus::RUNNING_HEALTHY_UNCLAIMED, + PolydockAppInstanceStatus::RUNNING_HEALTHY_CLAIMED, + PolydockAppInstanceStatus::RUNNING_UNHEALTHY, + PolydockAppInstanceStatus::RUNNING_UNRESPONSIVE, + PolydockAppInstanceStatus::PENDING_PRE_REMOVE, + PolydockAppInstanceStatus::PRE_REMOVE_RUNNING, + PolydockAppInstanceStatus::PRE_REMOVE_COMPLETED, + PolydockAppInstanceStatus::PENDING_REMOVE, + PolydockAppInstanceStatus::REMOVE_RUNNING, + PolydockAppInstanceStatus::REMOVE_COMPLETED, + PolydockAppInstanceStatus::PENDING_POST_REMOVE, + PolydockAppInstanceStatus::POST_REMOVE_RUNNING, + PolydockAppInstanceStatus::POST_REMOVE_COMPLETED, + PolydockAppInstanceStatus::REMOVED, + ]; + + $expectedIndex = array_search($expectedStatus, $statusOrder, true); + $currentIndex = array_search($currentStatus, $statusOrder, true); + + if ($expectedIndex === false || $currentIndex === false) { + return false; + } + + return $currentIndex > $expectedIndex; + } + public function polydockJobStart() { $this->appInstance = PolydockAppInstance::find($this->appInstanceId)->refresh(); diff --git a/app/Jobs/ProcessPolydockAppInstanceJobs/Claim/ClaimJob.php b/app/Jobs/ProcessPolydockAppInstanceJobs/Claim/ClaimJob.php index fdc6cdc5..b04144e0 100644 --- a/app/Jobs/ProcessPolydockAppInstanceJobs/Claim/ClaimJob.php +++ b/app/Jobs/ProcessPolydockAppInstanceJobs/Claim/ClaimJob.php @@ -25,6 +25,12 @@ public function handle(): void } if ($appInstance->status != PolydockAppInstanceStatus::PENDING_POLYDOCK_CLAIM) { + if ($this->shouldSkipBecauseStatusAdvanced(PolydockAppInstanceStatus::PENDING_POLYDOCK_CLAIM)) { + $this->polydockJobDone(); + + return; + } + throw new PolydockAppInstanceStatusFlowException( 'ClaimJob must be in status PENDING_POLYDOCK_CLAIM', $appInstance->status, diff --git a/app/Jobs/ProcessPolydockAppInstanceJobs/Create/CreateJob.php b/app/Jobs/ProcessPolydockAppInstanceJobs/Create/CreateJob.php index ad837ea8..b5953a4c 100644 --- a/app/Jobs/ProcessPolydockAppInstanceJobs/Create/CreateJob.php +++ b/app/Jobs/ProcessPolydockAppInstanceJobs/Create/CreateJob.php @@ -28,6 +28,12 @@ public function handle(): void } if ($appInstance->status != PolydockAppInstanceStatus::PENDING_CREATE) { + if ($this->shouldSkipBecauseStatusAdvanced(PolydockAppInstanceStatus::PENDING_CREATE)) { + $this->polydockJobDone(); + + return; + } + throw new PolydockAppInstanceStatusFlowException( 'CreateJob must be in status PENDING_CREATE', ); diff --git a/app/Jobs/ProcessPolydockAppInstanceJobs/Create/PostCreateJob.php b/app/Jobs/ProcessPolydockAppInstanceJobs/Create/PostCreateJob.php index 57cb463b..54532fa4 100644 --- a/app/Jobs/ProcessPolydockAppInstanceJobs/Create/PostCreateJob.php +++ b/app/Jobs/ProcessPolydockAppInstanceJobs/Create/PostCreateJob.php @@ -27,6 +27,12 @@ public function handle(): void } if ($appInstance->status != PolydockAppInstanceStatus::PENDING_POST_CREATE) { + if ($this->shouldSkipBecauseStatusAdvanced(PolydockAppInstanceStatus::PENDING_POST_CREATE)) { + $this->polydockJobDone(); + + return; + } + throw new PolydockAppInstanceStatusFlowException( 'PostCreateJob must be in status PENDING_POST_CREATE', ); diff --git a/app/Jobs/ProcessPolydockAppInstanceJobs/Create/PreCreateJob.php b/app/Jobs/ProcessPolydockAppInstanceJobs/Create/PreCreateJob.php index a481f563..6b68996b 100644 --- a/app/Jobs/ProcessPolydockAppInstanceJobs/Create/PreCreateJob.php +++ b/app/Jobs/ProcessPolydockAppInstanceJobs/Create/PreCreateJob.php @@ -28,6 +28,12 @@ public function handle(): void } if ($appInstance->status != PolydockAppInstanceStatus::PENDING_PRE_CREATE) { + if ($this->shouldSkipBecauseStatusAdvanced(PolydockAppInstanceStatus::PENDING_PRE_CREATE)) { + $this->polydockJobDone(); + + return; + } + throw new PolydockAppInstanceStatusFlowException( 'PreCreateJob must be in status PENDING_PRE_CREATE', ); diff --git a/app/Jobs/ProcessPolydockAppInstanceJobs/Deploy/DeployJob.php b/app/Jobs/ProcessPolydockAppInstanceJobs/Deploy/DeployJob.php index 19b9c329..e1d50537 100644 --- a/app/Jobs/ProcessPolydockAppInstanceJobs/Deploy/DeployJob.php +++ b/app/Jobs/ProcessPolydockAppInstanceJobs/Deploy/DeployJob.php @@ -27,6 +27,12 @@ public function handle(): void } if ($appInstance->status != PolydockAppInstanceStatus::PENDING_DEPLOY) { + if ($this->shouldSkipBecauseStatusAdvanced(PolydockAppInstanceStatus::PENDING_DEPLOY)) { + $this->polydockJobDone(); + + return; + } + throw new PolydockAppInstanceStatusFlowException( 'DeployJob must be in status PENDING_DEPLOY', ); diff --git a/app/Jobs/ProcessPolydockAppInstanceJobs/Deploy/PostDeployJob.php b/app/Jobs/ProcessPolydockAppInstanceJobs/Deploy/PostDeployJob.php index 4e82487e..2c261de5 100644 --- a/app/Jobs/ProcessPolydockAppInstanceJobs/Deploy/PostDeployJob.php +++ b/app/Jobs/ProcessPolydockAppInstanceJobs/Deploy/PostDeployJob.php @@ -28,6 +28,12 @@ public function handle(): void } if ($appInstance->status != PolydockAppInstanceStatus::PENDING_POST_DEPLOY) { + if ($this->shouldSkipBecauseStatusAdvanced(PolydockAppInstanceStatus::PENDING_POST_DEPLOY)) { + $this->polydockJobDone(); + + return; + } + throw new PolydockAppInstanceStatusFlowException( 'PostDeployJob must be in status PENDING_POST_DEPLOY', ); diff --git a/app/Jobs/ProcessPolydockAppInstanceJobs/Deploy/PreDeployJob.php b/app/Jobs/ProcessPolydockAppInstanceJobs/Deploy/PreDeployJob.php index d5ea380e..48d930b4 100644 --- a/app/Jobs/ProcessPolydockAppInstanceJobs/Deploy/PreDeployJob.php +++ b/app/Jobs/ProcessPolydockAppInstanceJobs/Deploy/PreDeployJob.php @@ -27,6 +27,12 @@ public function handle(): void } if ($appInstance->status != PolydockAppInstanceStatus::PENDING_PRE_DEPLOY) { + if ($this->shouldSkipBecauseStatusAdvanced(PolydockAppInstanceStatus::PENDING_PRE_DEPLOY)) { + $this->polydockJobDone(); + + return; + } + throw new PolydockAppInstanceStatusFlowException( 'PreDeployJob must be in status PENDING_PRE_DEPLOY', ); diff --git a/app/Jobs/ProcessPolydockAppInstanceJobs/New/ProcessNewJob.php b/app/Jobs/ProcessPolydockAppInstanceJobs/New/ProcessNewJob.php index ae91b709..6887bf8f 100644 --- a/app/Jobs/ProcessPolydockAppInstanceJobs/New/ProcessNewJob.php +++ b/app/Jobs/ProcessPolydockAppInstanceJobs/New/ProcessNewJob.php @@ -25,6 +25,12 @@ public function handle(): void } if ($appInstance->status != PolydockAppInstanceStatus::NEW) { + if ($this->shouldSkipBecauseStatusAdvanced(PolydockAppInstanceStatus::NEW)) { + $this->polydockJobDone(); + + return; + } + throw new PolydockAppInstanceStatusFlowException( 'New PolydockAppInstance must be in status NEW', ); diff --git a/app/Jobs/ProcessPolydockAppInstanceJobs/Remove/PostRemoveJob.php b/app/Jobs/ProcessPolydockAppInstanceJobs/Remove/PostRemoveJob.php index 6afd5d1a..116f415f 100644 --- a/app/Jobs/ProcessPolydockAppInstanceJobs/Remove/PostRemoveJob.php +++ b/app/Jobs/ProcessPolydockAppInstanceJobs/Remove/PostRemoveJob.php @@ -27,6 +27,12 @@ public function handle(): void } if ($appInstance->status != PolydockAppInstanceStatus::PENDING_POST_REMOVE) { + if ($this->shouldSkipBecauseStatusAdvanced(PolydockAppInstanceStatus::PENDING_POST_REMOVE)) { + $this->polydockJobDone(); + + return; + } + throw new PolydockAppInstanceStatusFlowException( 'PostRemoveJob must be in status PENDING_POST_REMOVE', ); diff --git a/app/Jobs/ProcessPolydockAppInstanceJobs/Remove/PreRemoveJob.php b/app/Jobs/ProcessPolydockAppInstanceJobs/Remove/PreRemoveJob.php index 1c45dd53..e07b9a71 100644 --- a/app/Jobs/ProcessPolydockAppInstanceJobs/Remove/PreRemoveJob.php +++ b/app/Jobs/ProcessPolydockAppInstanceJobs/Remove/PreRemoveJob.php @@ -27,6 +27,12 @@ public function handle(): void } if ($appInstance->status != PolydockAppInstanceStatus::PENDING_PRE_REMOVE) { + if ($this->shouldSkipBecauseStatusAdvanced(PolydockAppInstanceStatus::PENDING_PRE_REMOVE)) { + $this->polydockJobDone(); + + return; + } + throw new PolydockAppInstanceStatusFlowException( 'PreRemoveJob must be in status PENDING_PRE_REMOVE', ); diff --git a/app/Jobs/ProcessPolydockAppInstanceJobs/Remove/RemoveJob.php b/app/Jobs/ProcessPolydockAppInstanceJobs/Remove/RemoveJob.php index 124f65f0..466fefdc 100644 --- a/app/Jobs/ProcessPolydockAppInstanceJobs/Remove/RemoveJob.php +++ b/app/Jobs/ProcessPolydockAppInstanceJobs/Remove/RemoveJob.php @@ -27,6 +27,12 @@ public function handle(): void } if ($appInstance->status != PolydockAppInstanceStatus::PENDING_REMOVE) { + if ($this->shouldSkipBecauseStatusAdvanced(PolydockAppInstanceStatus::PENDING_REMOVE)) { + $this->polydockJobDone(); + + return; + } + throw new PolydockAppInstanceStatusFlowException( 'RemoveJob must be in status PENDING_REMOVE', ); diff --git a/tests/Feature/Jobs/StaleLifecycleJobTest.php b/tests/Feature/Jobs/StaleLifecycleJobTest.php new file mode 100644 index 00000000..ca027247 --- /dev/null +++ b/tests/Feature/Jobs/StaleLifecycleJobTest.php @@ -0,0 +1,47 @@ +create(); + $storeApp = PolydockStoreApp::factory()->create([ + 'polydock_store_id' => $store->id, + ]); + $userGroup = UserGroup::factory()->create(); + + $appInstance = PolydockAppInstance::createQuietly([ + 'polydock_store_app_id' => $storeApp->id, + 'user_group_id' => $userGroup->id, + 'name' => 'stale-claim-job-test', + 'app_type' => PolydockAiApp::class, + 'status' => PolydockAppInstanceStatus::RUNNING_HEALTHY_CLAIMED, + 'status_message' => 'Already running', + 'data' => [], + ]); + + $job = new ClaimJob($appInstance->id); + $job->handle(); + + $appInstance->refresh(); + + $this->assertSame(PolydockAppInstanceStatus::RUNNING_HEALTHY_CLAIMED, $appInstance->status); + $this->assertSame('Already running', $appInstance->status_message); + } +} From 2c886df2bf2c5b819fb689a6f188976e843194a9 Mon Sep 17 00:00:00 2001 From: Dan Lemon Date: Tue, 28 Apr 2026 18:34:21 +0300 Subject: [PATCH 2/7] chore: pr comments --- .../BaseJob.php | 48 ++++++- tests/Feature/Jobs/StaleLifecycleJobTest.php | 122 ++++++++++++++++-- 2 files changed, 156 insertions(+), 14 deletions(-) diff --git a/app/Jobs/ProcessPolydockAppInstanceJobs/BaseJob.php b/app/Jobs/ProcessPolydockAppInstanceJobs/BaseJob.php index 1d5b65df..a455a011 100644 --- a/app/Jobs/ProcessPolydockAppInstanceJobs/BaseJob.php +++ b/app/Jobs/ProcessPolydockAppInstanceJobs/BaseJob.php @@ -116,46 +116,90 @@ protected function shouldSkipBecauseStatusAdvanced(PolydockAppInstanceStatus $ex return true; } - private function isKnownStatusProgression(PolydockAppInstanceStatus $expectedStatus, PolydockAppInstanceStatus $currentStatus): bool + /** + * Canonical ordering of lifecycle statuses, used to detect when a queued + * job is stale because the instance has already advanced past the status + * the job was scheduled for. + * + * Keep this list in sync with the status flow handled by + * {@see \App\PolydockEngine\Engine}, the dispatch table in + * {@see \App\Listeners\ProcessPolydockAppInstanceStatusChange}, and the + * stage groupings on {@see \App\Models\PolydockAppInstance}. + * + * Upgrade statuses sit after the running/claimed states because an + * in-place upgrade is initiated against an already-claimed, running + * instance and returns to a running/claimed state when complete. + * + * @return list + */ + private static function lifecycleStatusOrder(): array { - $statusOrder = [ + return [ + // New / pre-create PolydockAppInstanceStatus::NEW, PolydockAppInstanceStatus::PENDING_PRE_CREATE, PolydockAppInstanceStatus::PRE_CREATE_RUNNING, PolydockAppInstanceStatus::PRE_CREATE_COMPLETED, + // Create PolydockAppInstanceStatus::PENDING_CREATE, PolydockAppInstanceStatus::CREATE_RUNNING, PolydockAppInstanceStatus::CREATE_COMPLETED, + // Post-create PolydockAppInstanceStatus::PENDING_POST_CREATE, PolydockAppInstanceStatus::POST_CREATE_RUNNING, PolydockAppInstanceStatus::POST_CREATE_COMPLETED, + // Pre-deploy PolydockAppInstanceStatus::PENDING_PRE_DEPLOY, PolydockAppInstanceStatus::PRE_DEPLOY_RUNNING, PolydockAppInstanceStatus::PRE_DEPLOY_COMPLETED, + // Deploy PolydockAppInstanceStatus::PENDING_DEPLOY, PolydockAppInstanceStatus::DEPLOY_RUNNING, PolydockAppInstanceStatus::DEPLOY_COMPLETED, + // Post-deploy PolydockAppInstanceStatus::PENDING_POST_DEPLOY, PolydockAppInstanceStatus::POST_DEPLOY_RUNNING, PolydockAppInstanceStatus::POST_DEPLOY_COMPLETED, + // Claim PolydockAppInstanceStatus::PENDING_POLYDOCK_CLAIM, PolydockAppInstanceStatus::POLYDOCK_CLAIM_RUNNING, PolydockAppInstanceStatus::POLYDOCK_CLAIM_COMPLETED, + // Running PolydockAppInstanceStatus::RUNNING_HEALTHY_UNCLAIMED, PolydockAppInstanceStatus::RUNNING_HEALTHY_CLAIMED, PolydockAppInstanceStatus::RUNNING_UNHEALTHY, PolydockAppInstanceStatus::RUNNING_UNRESPONSIVE, + // Pre-upgrade (in-place upgrade against a running, claimed instance) + PolydockAppInstanceStatus::PENDING_PRE_UPGRADE, + PolydockAppInstanceStatus::PRE_UPGRADE_RUNNING, + PolydockAppInstanceStatus::PRE_UPGRADE_COMPLETED, + // Upgrade + PolydockAppInstanceStatus::PENDING_UPGRADE, + PolydockAppInstanceStatus::UPGRADE_RUNNING, + PolydockAppInstanceStatus::UPGRADE_COMPLETED, + // Post-upgrade + PolydockAppInstanceStatus::PENDING_POST_UPGRADE, + PolydockAppInstanceStatus::POST_UPGRADE_RUNNING, + PolydockAppInstanceStatus::POST_UPGRADE_COMPLETED, + // Pre-remove PolydockAppInstanceStatus::PENDING_PRE_REMOVE, PolydockAppInstanceStatus::PRE_REMOVE_RUNNING, PolydockAppInstanceStatus::PRE_REMOVE_COMPLETED, + // Remove PolydockAppInstanceStatus::PENDING_REMOVE, PolydockAppInstanceStatus::REMOVE_RUNNING, PolydockAppInstanceStatus::REMOVE_COMPLETED, + // Post-remove PolydockAppInstanceStatus::PENDING_POST_REMOVE, PolydockAppInstanceStatus::POST_REMOVE_RUNNING, PolydockAppInstanceStatus::POST_REMOVE_COMPLETED, PolydockAppInstanceStatus::REMOVED, ]; + } + + private function isKnownStatusProgression(PolydockAppInstanceStatus $expectedStatus, PolydockAppInstanceStatus $currentStatus): bool + { + $statusOrder = self::lifecycleStatusOrder(); $expectedIndex = array_search($expectedStatus, $statusOrder, true); $currentIndex = array_search($currentStatus, $statusOrder, true); diff --git a/tests/Feature/Jobs/StaleLifecycleJobTest.php b/tests/Feature/Jobs/StaleLifecycleJobTest.php index ca027247..080ba6ec 100644 --- a/tests/Feature/Jobs/StaleLifecycleJobTest.php +++ b/tests/Feature/Jobs/StaleLifecycleJobTest.php @@ -5,11 +5,15 @@ namespace Tests\Feature\Jobs; use App\Jobs\ProcessPolydockAppInstanceJobs\Claim\ClaimJob; +use App\Jobs\ProcessPolydockAppInstanceJobs\Create\CreateJob; +use App\Jobs\ProcessPolydockAppInstanceJobs\Deploy\DeployJob; +use App\Jobs\ProcessPolydockAppInstanceJobs\Remove\RemoveJob; use App\Models\PolydockAppInstance; use App\Models\PolydockStore; use App\Models\PolydockStoreApp; use App\Models\UserGroup; use FreedomtechHosting\PolydockApp\Enums\PolydockAppInstanceStatus; +use FreedomtechHosting\PolydockApp\PolydockAppInstanceStatusFlowException; use FreedomtechHosting\PolydockAppAmazeeioGeneric\PolydockAiApp; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; @@ -20,28 +24,122 @@ class StaleLifecycleJobTest extends TestCase public function test_claim_job_skips_when_instance_already_advanced_to_running(): void { + $appInstance = $this->createAppInstance( + 'stale-claim-job-test', + PolydockAppInstanceStatus::RUNNING_HEALTHY_CLAIMED, + 'Already running', + ); + + (new ClaimJob($appInstance->id))->handle(); + + $appInstance->refresh(); + + $this->assertSame(PolydockAppInstanceStatus::RUNNING_HEALTHY_CLAIMED, $appInstance->status); + $this->assertSame('Already running', $appInstance->status_message); + } + + public function test_claim_job_skips_when_instance_already_advanced_to_upgrade_status(): void + { + // A stale ClaimJob can land in the queue after the instance has + // already been claimed AND begun an in-place upgrade. The skip + // logic must recognise upgrade statuses as "advanced past claim". + $appInstance = $this->createAppInstance( + 'stale-claim-during-upgrade', + PolydockAppInstanceStatus::PENDING_PRE_UPGRADE, + 'Upgrade pending', + ); + + (new ClaimJob($appInstance->id))->handle(); + + $appInstance->refresh(); + + $this->assertSame(PolydockAppInstanceStatus::PENDING_PRE_UPGRADE, $appInstance->status); + $this->assertSame('Upgrade pending', $appInstance->status_message); + } + + public function test_create_job_skips_when_instance_already_advanced_to_deploy(): void + { + $appInstance = $this->createAppInstance( + 'stale-create-job-test', + PolydockAppInstanceStatus::PENDING_DEPLOY, + 'Ready to deploy', + ); + + (new CreateJob($appInstance->id))->handle(); + + $appInstance->refresh(); + + $this->assertSame(PolydockAppInstanceStatus::PENDING_DEPLOY, $appInstance->status); + $this->assertSame('Ready to deploy', $appInstance->status_message); + } + + public function test_deploy_job_skips_when_instance_already_advanced_to_running(): void + { + $appInstance = $this->createAppInstance( + 'stale-deploy-job-test', + PolydockAppInstanceStatus::RUNNING_HEALTHY_UNCLAIMED, + 'Running unclaimed', + ); + + (new DeployJob($appInstance->id))->handle(); + + $appInstance->refresh(); + + $this->assertSame(PolydockAppInstanceStatus::RUNNING_HEALTHY_UNCLAIMED, $appInstance->status); + $this->assertSame('Running unclaimed', $appInstance->status_message); + } + + public function test_remove_job_skips_when_instance_already_advanced_to_post_remove(): void + { + $appInstance = $this->createAppInstance( + 'stale-remove-job-test', + PolydockAppInstanceStatus::PENDING_POST_REMOVE, + 'Awaiting post-remove', + ); + + (new RemoveJob($appInstance->id))->handle(); + + $appInstance->refresh(); + + $this->assertSame(PolydockAppInstanceStatus::PENDING_POST_REMOVE, $appInstance->status); + $this->assertSame('Awaiting post-remove', $appInstance->status_message); + } + + public function test_create_job_throws_status_flow_exception_when_status_is_not_a_known_progression(): void + { + // Sanity check: the skip logic must only short-circuit when the + // current status is genuinely *after* the expected status. An + // unrelated/earlier status must still raise the flow exception. + $appInstance = $this->createAppInstance( + 'unrelated-status-create-job', + PolydockAppInstanceStatus::NEW, + 'Brand new', + ); + + $this->expectException(PolydockAppInstanceStatusFlowException::class); + + (new CreateJob($appInstance->id))->handle(); + } + + private function createAppInstance( + string $name, + PolydockAppInstanceStatus $status, + string $statusMessage, + ): PolydockAppInstance { $store = PolydockStore::factory()->create(); $storeApp = PolydockStoreApp::factory()->create([ 'polydock_store_id' => $store->id, ]); $userGroup = UserGroup::factory()->create(); - $appInstance = PolydockAppInstance::createQuietly([ + return PolydockAppInstance::createQuietly([ 'polydock_store_app_id' => $storeApp->id, 'user_group_id' => $userGroup->id, - 'name' => 'stale-claim-job-test', + 'name' => $name, 'app_type' => PolydockAiApp::class, - 'status' => PolydockAppInstanceStatus::RUNNING_HEALTHY_CLAIMED, - 'status_message' => 'Already running', + 'status' => $status, + 'status_message' => $statusMessage, 'data' => [], ]); - - $job = new ClaimJob($appInstance->id); - $job->handle(); - - $appInstance->refresh(); - - $this->assertSame(PolydockAppInstanceStatus::RUNNING_HEALTHY_CLAIMED, $appInstance->status); - $this->assertSame('Already running', $appInstance->status_message); } } From ad978a8d62842dc58eb7a36e810e81ae4809e737 Mon Sep 17 00:00:00 2001 From: Dan Lemon Date: Wed, 29 Apr 2026 14:12:55 +0200 Subject: [PATCH 3/7] Update app/Jobs/ProcessPolydockAppInstanceJobs/BaseJob.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../BaseJob.php | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/app/Jobs/ProcessPolydockAppInstanceJobs/BaseJob.php b/app/Jobs/ProcessPolydockAppInstanceJobs/BaseJob.php index a455a011..de5dded8 100644 --- a/app/Jobs/ProcessPolydockAppInstanceJobs/BaseJob.php +++ b/app/Jobs/ProcessPolydockAppInstanceJobs/BaseJob.php @@ -197,18 +197,30 @@ private static function lifecycleStatusOrder(): array ]; } - private function isKnownStatusProgression(PolydockAppInstanceStatus $expectedStatus, PolydockAppInstanceStatus $currentStatus): bool + private static function lifecycleStatusIndexMap(): array { - $statusOrder = self::lifecycleStatusOrder(); + static $statusIndexMap = null; + + if ($statusIndexMap === null) { + $statusIndexMap = []; + + foreach (self::lifecycleStatusOrder() as $index => $status) { + $statusIndexMap[$status->value] = $index; + } + } - $expectedIndex = array_search($expectedStatus, $statusOrder, true); - $currentIndex = array_search($currentStatus, $statusOrder, true); + return $statusIndexMap; + } + + private function isKnownStatusProgression(PolydockAppInstanceStatus $expectedStatus, PolydockAppInstanceStatus $currentStatus): bool + { + $statusIndexMap = self::lifecycleStatusIndexMap(); - if ($expectedIndex === false || $currentIndex === false) { + if (! array_key_exists($expectedStatus->value, $statusIndexMap) || ! array_key_exists($currentStatus->value, $statusIndexMap)) { return false; } - return $currentIndex > $expectedIndex; + return $statusIndexMap[$currentStatus->value] > $statusIndexMap[$expectedStatus->value]; } public function polydockJobStart() From c80f28f0fcc11c5008068fed17883a5754dc726c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 29 Apr 2026 12:22:07 +0000 Subject: [PATCH 4/7] chore: expand stale-job skip test coverage Agent-Logs-Url: https://github.com/amazeeio/polydock-engine/sessions/3248445d-d7b5-4ba8-8c8a-da2406e6c1e9 Co-authored-by: dan2k3k4 <158704+dan2k3k4@users.noreply.github.com> --- composer.lock | 459 ++++++++++++++++++++++++++------------------------ 1 file changed, 237 insertions(+), 222 deletions(-) diff --git a/composer.lock b/composer.lock index 23eee3b3..d8dc5658 100644 --- a/composer.lock +++ b/composer.lock @@ -55,16 +55,16 @@ }, { "name": "amazeeio/polydock-app-amazeeclaw", - "version": "v0.1.11", + "version": "v0.1.12", "source": { "type": "git", "url": "https://github.com/amazeeio/polydock-app-amazeeclaw.git", - "reference": "332d956eba34bf8f0fb708383f169594c49c32f1" + "reference": "9c39096f41d23294a78fe6ba50d55f6acc210bb9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amazeeio/polydock-app-amazeeclaw/zipball/332d956eba34bf8f0fb708383f169594c49c32f1", - "reference": "332d956eba34bf8f0fb708383f169594c49c32f1", + "url": "https://api.github.com/repos/amazeeio/polydock-app-amazeeclaw/zipball/9c39096f41d23294a78fe6ba50d55f6acc210bb9", + "reference": "9c39096f41d23294a78fe6ba50d55f6acc210bb9", "shasum": "" }, "require": { @@ -83,9 +83,9 @@ "description": "Polydock App - AmazeeClaw AI", "support": { "issues": "https://github.com/amazeeio/polydock-app-amazeeclaw/issues", - "source": "https://github.com/amazeeio/polydock-app-amazeeclaw/tree/v0.1.11" + "source": "https://github.com/amazeeio/polydock-app-amazeeclaw/tree/v0.1.12" }, - "time": "2026-03-23T19:47:52+00:00" + "time": "2026-04-27T09:29:47+00:00" }, { "name": "amazeeio/polydock-app-amazeeio-privategpt", @@ -364,16 +364,16 @@ }, { "name": "blade-ui-kit/blade-icons", - "version": "1.9.1", + "version": "1.10.0", "source": { "type": "git", "url": "https://github.com/driesvints/blade-icons.git", - "reference": "377eede719f9690b03bbbfd516afef887e27634a" + "reference": "74189a80bbaa4966aebaee54fec3a3c2ef0a5f3a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/driesvints/blade-icons/zipball/377eede719f9690b03bbbfd516afef887e27634a", - "reference": "377eede719f9690b03bbbfd516afef887e27634a", + "url": "https://api.github.com/repos/driesvints/blade-icons/zipball/74189a80bbaa4966aebaee54fec3a3c2ef0a5f3a", + "reference": "74189a80bbaa4966aebaee54fec3a3c2ef0a5f3a", "shasum": "" }, "require": { @@ -441,7 +441,7 @@ "type": "paypal" } ], - "time": "2026-04-07T17:56:48+00:00" + "time": "2026-04-23T19:03:45+00:00" }, { "name": "brick/math", @@ -756,16 +756,16 @@ }, { "name": "dedoc/scramble", - "version": "v0.13.18", + "version": "v0.13.22", "source": { "type": "git", "url": "https://github.com/dedoc/scramble.git", - "reference": "e013e242dfea2791c856330f8abbfd59a3a5bbc1" + "reference": "b54b0c43bdebaa01f66cc3dfdd8f91e19b12da96" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dedoc/scramble/zipball/e013e242dfea2791c856330f8abbfd59a3a5bbc1", - "reference": "e013e242dfea2791c856330f8abbfd59a3a5bbc1", + "url": "https://api.github.com/repos/dedoc/scramble/zipball/b54b0c43bdebaa01f66cc3dfdd8f91e19b12da96", + "reference": "b54b0c43bdebaa01f66cc3dfdd8f91e19b12da96", "shasum": "" }, "require": { @@ -824,7 +824,7 @@ ], "support": { "issues": "https://github.com/dedoc/scramble/issues", - "source": "https://github.com/dedoc/scramble/tree/v0.13.18" + "source": "https://github.com/dedoc/scramble/tree/v0.13.22" }, "funding": [ { @@ -832,7 +832,7 @@ "type": "github" } ], - "time": "2026-04-10T17:59:04+00:00" + "time": "2026-04-27T20:30:51+00:00" }, { "name": "dflydev/dot-access-data", @@ -2635,16 +2635,16 @@ }, { "name": "laravel/framework", - "version": "v12.56.0", + "version": "v12.58.0", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "dac16d424b59debb2273910dde88eb7050a2a709" + "reference": "6172ae1f44ba5d89e111057ee4a4e7c27f5a610d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/dac16d424b59debb2273910dde88eb7050a2a709", - "reference": "dac16d424b59debb2273910dde88eb7050a2a709", + "url": "https://api.github.com/repos/laravel/framework/zipball/6172ae1f44ba5d89e111057ee4a4e7c27f5a610d", + "reference": "6172ae1f44ba5d89e111057ee4a4e7c27f5a610d", "shasum": "" }, "require": { @@ -2685,8 +2685,8 @@ "symfony/mailer": "^7.2.0", "symfony/mime": "^7.2.0", "symfony/polyfill-php83": "^1.33", - "symfony/polyfill-php84": "^1.33", - "symfony/polyfill-php85": "^1.33", + "symfony/polyfill-php84": "^1.34", + "symfony/polyfill-php85": "^1.34", "symfony/process": "^7.2.0", "symfony/routing": "^7.2.0", "symfony/uid": "^7.2.0", @@ -2853,20 +2853,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2026-03-26T14:51:54+00:00" + "time": "2026-04-26T16:42:04+00:00" }, { "name": "laravel/horizon", - "version": "v5.45.6", + "version": "v5.46.0", "source": { "type": "git", "url": "https://github.com/laravel/horizon.git", - "reference": "629707ff3cce9ff72715e7815f74dda10bdcbe0a" + "reference": "bfea968e8aa674fb649d02e55ea0d38bdf5137d5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/horizon/zipball/629707ff3cce9ff72715e7815f74dda10bdcbe0a", - "reference": "629707ff3cce9ff72715e7815f74dda10bdcbe0a", + "url": "https://api.github.com/repos/laravel/horizon/zipball/bfea968e8aa674fb649d02e55ea0d38bdf5137d5", + "reference": "bfea968e8aa674fb649d02e55ea0d38bdf5137d5", "shasum": "" }, "require": { @@ -2931,22 +2931,22 @@ ], "support": { "issues": "https://github.com/laravel/horizon/issues", - "source": "https://github.com/laravel/horizon/tree/v5.45.6" + "source": "https://github.com/laravel/horizon/tree/v5.46.0" }, - "time": "2026-04-14T13:31:53+00:00" + "time": "2026-04-20T18:08:11+00:00" }, { "name": "laravel/prompts", - "version": "v0.3.16", + "version": "v0.3.17", "source": { "type": "git", "url": "https://github.com/laravel/prompts.git", - "reference": "11e7d5f93803a2190b00e145142cb00a33d17ad2" + "reference": "6a82ac19a28b916ae0885828795dbd4c59d9a818" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/prompts/zipball/11e7d5f93803a2190b00e145142cb00a33d17ad2", - "reference": "11e7d5f93803a2190b00e145142cb00a33d17ad2", + "url": "https://api.github.com/repos/laravel/prompts/zipball/6a82ac19a28b916ae0885828795dbd4c59d9a818", + "reference": "6a82ac19a28b916ae0885828795dbd4c59d9a818", "shasum": "" }, "require": { @@ -2990,9 +2990,9 @@ "description": "Add beautiful and user-friendly forms to your command-line applications.", "support": { "issues": "https://github.com/laravel/prompts/issues", - "source": "https://github.com/laravel/prompts/tree/v0.3.16" + "source": "https://github.com/laravel/prompts/tree/v0.3.17" }, - "time": "2026-03-23T14:35:33+00:00" + "time": "2026-04-20T16:07:33+00:00" }, { "name": "laravel/sanctum", @@ -3115,16 +3115,16 @@ }, { "name": "laravel/serializable-closure", - "version": "v2.0.12", + "version": "v2.0.13", "source": { "type": "git", "url": "https://github.com/laravel/serializable-closure.git", - "reference": "a6abb4e54f6fcd3138120b9ad497f0bd146f9919" + "reference": "b566ee0dd251f3c4078bed003a7ce015f5ea6dce" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/a6abb4e54f6fcd3138120b9ad497f0bd146f9919", - "reference": "a6abb4e54f6fcd3138120b9ad497f0bd146f9919", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/b566ee0dd251f3c4078bed003a7ce015f5ea6dce", + "reference": "b566ee0dd251f3c4078bed003a7ce015f5ea6dce", "shasum": "" }, "require": { @@ -3172,7 +3172,7 @@ "issues": "https://github.com/laravel/serializable-closure/issues", "source": "https://github.com/laravel/serializable-closure" }, - "time": "2026-04-14T13:33:34+00:00" + "time": "2026-04-16T14:03:50+00:00" }, { "name": "laravel/slack-notification-channel", @@ -5023,16 +5023,16 @@ }, { "name": "phpseclib/phpseclib", - "version": "3.0.51", + "version": "3.0.52", "source": { "type": "git", "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "d59c94077f9c9915abb51ddb52ce85188ece1748" + "reference": "2adaefc83df2ec548558307690f376dd7d4f4fce" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/d59c94077f9c9915abb51ddb52ce85188ece1748", - "reference": "d59c94077f9c9915abb51ddb52ce85188ece1748", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/2adaefc83df2ec548558307690f376dd7d4f4fce", + "reference": "2adaefc83df2ec548558307690f376dd7d4f4fce", "shasum": "" }, "require": { @@ -5113,7 +5113,7 @@ ], "support": { "issues": "https://github.com/phpseclib/phpseclib/issues", - "source": "https://github.com/phpseclib/phpseclib/tree/3.0.51" + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.52" }, "funding": [ { @@ -5129,7 +5129,7 @@ "type": "tidelift" } ], - "time": "2026-04-10T01:33:53+00:00" + "time": "2026-04-27T07:02:15+00:00" }, { "name": "phpstan/phpdoc-parser", @@ -6478,21 +6478,22 @@ }, { "name": "symfony/clock", - "version": "v8.0.8", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/clock.git", - "reference": "b55a638b189a6faa875e0ccdb00908fb87af95b3" + "reference": "674fa3b98e21531dd040e613479f5f6fa8f32111" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/clock/zipball/b55a638b189a6faa875e0ccdb00908fb87af95b3", - "reference": "b55a638b189a6faa875e0ccdb00908fb87af95b3", + "url": "https://api.github.com/repos/symfony/clock/zipball/674fa3b98e21531dd040e613479f5f6fa8f32111", + "reference": "674fa3b98e21531dd040e613479f5f6fa8f32111", "shasum": "" }, "require": { - "php": ">=8.4", - "psr/clock": "^1.0" + "php": ">=8.2", + "psr/clock": "^1.0", + "symfony/polyfill-php83": "^1.28" }, "provide": { "psr/clock-implementation": "1.0" @@ -6531,7 +6532,7 @@ "time" ], "support": { - "source": "https://github.com/symfony/clock/tree/v8.0.8" + "source": "https://github.com/symfony/clock/tree/v7.4.8" }, "funding": [ { @@ -6551,7 +6552,7 @@ "type": "tidelift" } ], - "time": "2026-03-30T15:14:47+00:00" + "time": "2026-03-24T13:12:05+00:00" }, { "name": "symfony/console", @@ -6653,20 +6654,20 @@ }, { "name": "symfony/css-selector", - "version": "v8.0.8", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "8db1c00226a94d8ab6aa89d9224eeee91e2ea2ed" + "reference": "b055f228a4178a1d6774909903905e3475f3eac8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/8db1c00226a94d8ab6aa89d9224eeee91e2ea2ed", - "reference": "8db1c00226a94d8ab6aa89d9224eeee91e2ea2ed", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/b055f228a4178a1d6774909903905e3475f3eac8", + "reference": "b055f228a4178a1d6774909903905e3475f3eac8", "shasum": "" }, "require": { - "php": ">=8.4" + "php": ">=8.2" }, "type": "library", "autoload": { @@ -6698,7 +6699,7 @@ "description": "Converts CSS selectors to XPath expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/css-selector/tree/v8.0.8" + "source": "https://github.com/symfony/css-selector/tree/v7.4.8" }, "funding": [ { @@ -6718,7 +6719,7 @@ "type": "tidelift" } ], - "time": "2026-03-30T15:14:47+00:00" + "time": "2026-03-24T13:12:05+00:00" }, { "name": "symfony/deprecation-contracts", @@ -6871,24 +6872,24 @@ }, { "name": "symfony/event-dispatcher", - "version": "v8.0.8", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "f662acc6ab22a3d6d716dcb44c381c6002940df6" + "reference": "f57b899fa736fd71121168ef268f23c206083f0a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/f662acc6ab22a3d6d716dcb44c381c6002940df6", - "reference": "f662acc6ab22a3d6d716dcb44c381c6002940df6", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/f57b899fa736fd71121168ef268f23c206083f0a", + "reference": "f57b899fa736fd71121168ef268f23c206083f0a", "shasum": "" }, "require": { - "php": ">=8.4", + "php": ">=8.2", "symfony/event-dispatcher-contracts": "^2.5|^3" }, "conflict": { - "symfony/security-http": "<7.4", + "symfony/dependency-injection": "<6.4", "symfony/service-contracts": "<2.5" }, "provide": { @@ -6897,14 +6898,14 @@ }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^7.4|^8.0", - "symfony/dependency-injection": "^7.4|^8.0", - "symfony/error-handler": "^7.4|^8.0", - "symfony/expression-language": "^7.4|^8.0", - "symfony/framework-bundle": "^7.4|^8.0", - "symfony/http-foundation": "^7.4|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/error-handler": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/framework-bundle": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", "symfony/service-contracts": "^2.5|^3", - "symfony/stopwatch": "^7.4|^8.0" + "symfony/stopwatch": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -6932,7 +6933,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v8.0.8" + "source": "https://github.com/symfony/event-dispatcher/tree/v7.4.8" }, "funding": [ { @@ -6952,7 +6953,7 @@ "type": "tidelift" } ], - "time": "2026-03-30T15:14:47+00:00" + "time": "2026-03-30T13:54:39+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -7174,27 +7175,31 @@ }, { "name": "symfony/http-client", - "version": "v8.0.8", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "356e43d6994ae9d7761fd404d40f78691deabe0e" + "reference": "01933e626c3de76bea1e22641e205e78f6a34342" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/356e43d6994ae9d7761fd404d40f78691deabe0e", - "reference": "356e43d6994ae9d7761fd404d40f78691deabe0e", + "url": "https://api.github.com/repos/symfony/http-client/zipball/01933e626c3de76bea1e22641e205e78f6a34342", + "reference": "01933e626c3de76bea1e22641e205e78f6a34342", "shasum": "" }, "require": { - "php": ">=8.4", + "php": ">=8.2", "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/http-client-contracts": "~3.4.4|^3.5.2", + "symfony/polyfill-php83": "^1.29", "symfony/service-contracts": "^2.5|^3" }, "conflict": { - "amphp/amp": "<3", - "php-http/discovery": "<1.15" + "amphp/amp": "<2.5", + "amphp/socket": "<1.1", + "php-http/discovery": "<1.15", + "symfony/http-foundation": "<6.4" }, "provide": { "php-http/async-client-implementation": "*", @@ -7203,19 +7208,20 @@ "symfony/http-client-implementation": "3.0" }, "require-dev": { - "amphp/http-client": "^5.3.2", - "amphp/http-tunnel": "^2.0", + "amphp/http-client": "^4.2.1|^5.0", + "amphp/http-tunnel": "^1.0|^2.0", "guzzlehttp/promises": "^1.4|^2.0", "nyholm/psr7": "^1.0", "php-http/httplug": "^1.0|^2.0", "psr/http-client": "^1.0", - "symfony/cache": "^7.4|^8.0", - "symfony/dependency-injection": "^7.4|^8.0", - "symfony/http-kernel": "^7.4|^8.0", - "symfony/messenger": "^7.4|^8.0", - "symfony/process": "^7.4|^8.0", - "symfony/rate-limiter": "^7.4|^8.0", - "symfony/stopwatch": "^7.4|^8.0" + "symfony/amphp-http-client-meta": "^1.0|^2.0", + "symfony/cache": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/rate-limiter": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -7246,7 +7252,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v8.0.8" + "source": "https://github.com/symfony/http-client/tree/v7.4.8" }, "funding": [ { @@ -7266,7 +7272,7 @@ "type": "tidelift" } ], - "time": "2026-03-30T15:14:47+00:00" + "time": "2026-03-30T12:55:43+00:00" }, { "name": "symfony/http-client-contracts", @@ -7722,7 +7728,7 @@ }, { "name": "symfony/polyfill-ctype", - "version": "v1.36.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", @@ -7781,7 +7787,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.36.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0" }, "funding": [ { @@ -7805,16 +7811,16 @@ }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.36.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "ad1b7b9092976d6c948b8a187cec9faaea9ec1df" + "reference": "4864388bfbd3001ce88e234fab652acd91fdc57e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/ad1b7b9092976d6c948b8a187cec9faaea9ec1df", - "reference": "ad1b7b9092976d6c948b8a187cec9faaea9ec1df", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/4864388bfbd3001ce88e234fab652acd91fdc57e", + "reference": "4864388bfbd3001ce88e234fab652acd91fdc57e", "shasum": "" }, "require": { @@ -7863,7 +7869,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.36.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.37.0" }, "funding": [ { @@ -7883,11 +7889,11 @@ "type": "tidelift" } ], - "time": "2026-04-10T16:19:22+00:00" + "time": "2026-04-26T13:13:48+00:00" }, { "name": "symfony/polyfill-intl-idn", - "version": "v1.36.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-idn.git", @@ -7950,7 +7956,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.36.0" + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.37.0" }, "funding": [ { @@ -7974,7 +7980,7 @@ }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.36.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", @@ -8035,7 +8041,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.36.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.37.0" }, "funding": [ { @@ -8059,7 +8065,7 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.36.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", @@ -8120,7 +8126,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.36.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.37.0" }, "funding": [ { @@ -8144,7 +8150,7 @@ }, { "name": "symfony/polyfill-php80", - "version": "v1.36.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", @@ -8204,7 +8210,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.36.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.37.0" }, "funding": [ { @@ -8228,7 +8234,7 @@ }, { "name": "symfony/polyfill-php83", - "version": "v1.36.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php83.git", @@ -8284,7 +8290,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php83/tree/v1.36.0" + "source": "https://github.com/symfony/polyfill-php83/tree/v1.37.0" }, "funding": [ { @@ -8308,7 +8314,7 @@ }, { "name": "symfony/polyfill-php84", - "version": "v1.36.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php84.git", @@ -8364,7 +8370,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php84/tree/v1.36.0" + "source": "https://github.com/symfony/polyfill-php84/tree/v1.37.0" }, "funding": [ { @@ -8388,16 +8394,16 @@ }, { "name": "symfony/polyfill-php85", - "version": "v1.36.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php85.git", - "reference": "2c408a6bb0313e6001a83628dc5506100474254e" + "reference": "fcfa4973a9917cef23f2e38774da74a2b7d115ee" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/2c408a6bb0313e6001a83628dc5506100474254e", - "reference": "2c408a6bb0313e6001a83628dc5506100474254e", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/fcfa4973a9917cef23f2e38774da74a2b7d115ee", + "reference": "fcfa4973a9917cef23f2e38774da74a2b7d115ee", "shasum": "" }, "require": { @@ -8444,7 +8450,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php85/tree/v1.36.0" + "source": "https://github.com/symfony/polyfill-php85/tree/v1.37.0" }, "funding": [ { @@ -8464,11 +8470,11 @@ "type": "tidelift" } ], - "time": "2026-04-10T16:50:15+00:00" + "time": "2026-04-26T13:10:57+00:00" }, { "name": "symfony/polyfill-uuid", - "version": "v1.36.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-uuid.git", @@ -8527,7 +8533,7 @@ "uuid" ], "support": { - "source": "https://github.com/symfony/polyfill-uuid/tree/v1.36.0" + "source": "https://github.com/symfony/polyfill-uuid/tree/v1.37.0" }, "funding": [ { @@ -8788,34 +8794,35 @@ }, { "name": "symfony/string", - "version": "v8.0.8", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "ae9488f874d7603f9d2dfbf120203882b645d963" + "reference": "114ac57257d75df748eda23dd003878080b8e688" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/ae9488f874d7603f9d2dfbf120203882b645d963", - "reference": "ae9488f874d7603f9d2dfbf120203882b645d963", + "url": "https://api.github.com/repos/symfony/string/zipball/114ac57257d75df748eda23dd003878080b8e688", + "reference": "114ac57257d75df748eda23dd003878080b8e688", "shasum": "" }, "require": { - "php": ">=8.4", - "symfony/polyfill-ctype": "^1.8", - "symfony/polyfill-intl-grapheme": "^1.33", - "symfony/polyfill-intl-normalizer": "^1.0", - "symfony/polyfill-mbstring": "^1.0" + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.33", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" }, "conflict": { "symfony/translation-contracts": "<2.5" }, "require-dev": { - "symfony/emoji": "^7.4|^8.0", - "symfony/http-client": "^7.4|^8.0", - "symfony/intl": "^7.4|^8.0", + "symfony/emoji": "^7.1|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", "symfony/translation-contracts": "^2.5|^3.0", - "symfony/var-exporter": "^7.4|^8.0" + "symfony/var-exporter": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -8854,7 +8861,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v8.0.8" + "source": "https://github.com/symfony/string/tree/v7.4.8" }, "funding": [ { @@ -8874,31 +8881,38 @@ "type": "tidelift" } ], - "time": "2026-03-30T15:14:47+00:00" + "time": "2026-03-24T13:12:05+00:00" }, { "name": "symfony/translation", - "version": "v8.0.8", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "27c03ae3940de24ba2f71cfdbac824f2aa1fdf2f" + "reference": "33600f8489485425bfcddd0d983391038d3422e7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/27c03ae3940de24ba2f71cfdbac824f2aa1fdf2f", - "reference": "27c03ae3940de24ba2f71cfdbac824f2aa1fdf2f", + "url": "https://api.github.com/repos/symfony/translation/zipball/33600f8489485425bfcddd0d983391038d3422e7", + "reference": "33600f8489485425bfcddd0d983391038d3422e7", "shasum": "" }, "require": { - "php": ">=8.4", - "symfony/polyfill-mbstring": "^1.0", - "symfony/translation-contracts": "^3.6.1" + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/translation-contracts": "^2.5.3|^3.3" }, "conflict": { "nikic/php-parser": "<5.0", + "symfony/config": "<6.4", + "symfony/console": "<6.4", + "symfony/dependency-injection": "<6.4", "symfony/http-client-contracts": "<2.5", - "symfony/service-contracts": "<2.5" + "symfony/http-kernel": "<6.4", + "symfony/service-contracts": "<2.5", + "symfony/twig-bundle": "<6.4", + "symfony/yaml": "<6.4" }, "provide": { "symfony/translation-implementation": "2.3|3.0" @@ -8906,17 +8920,17 @@ "require-dev": { "nikic/php-parser": "^5.0", "psr/log": "^1|^2|^3", - "symfony/config": "^7.4|^8.0", - "symfony/console": "^7.4|^8.0", - "symfony/dependency-injection": "^7.4|^8.0", - "symfony/finder": "^7.4|^8.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", "symfony/http-client-contracts": "^2.5|^3.0", - "symfony/http-kernel": "^7.4|^8.0", - "symfony/intl": "^7.4|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", "symfony/polyfill-intl-icu": "^1.21", - "symfony/routing": "^7.4|^8.0", + "symfony/routing": "^6.4|^7.0|^8.0", "symfony/service-contracts": "^2.5|^3", - "symfony/yaml": "^7.4|^8.0" + "symfony/yaml": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -8947,7 +8961,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v8.0.8" + "source": "https://github.com/symfony/translation/tree/v7.4.8" }, "funding": [ { @@ -8967,7 +8981,7 @@ "type": "tidelift" } ], - "time": "2026-03-30T15:14:47+00:00" + "time": "2026-03-24T13:12:05+00:00" }, { "name": "symfony/translation-contracts", @@ -9357,23 +9371,23 @@ }, { "name": "voku/portable-ascii", - "version": "2.0.3", + "version": "2.1.1", "source": { "type": "git", "url": "https://github.com/voku/portable-ascii.git", - "reference": "b1d923f88091c6bf09699efcd7c8a1b1bfd7351d" + "reference": "8e1051fe39379367aecf014f41744ce7539a856f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/voku/portable-ascii/zipball/b1d923f88091c6bf09699efcd7c8a1b1bfd7351d", - "reference": "b1d923f88091c6bf09699efcd7c8a1b1bfd7351d", + "url": "https://api.github.com/repos/voku/portable-ascii/zipball/8e1051fe39379367aecf014f41744ce7539a856f", + "reference": "8e1051fe39379367aecf014f41744ce7539a856f", "shasum": "" }, "require": { - "php": ">=7.0.0" + "php": ">=7.1.0" }, "require-dev": { - "phpunit/phpunit": "~6.0 || ~7.0 || ~9.0" + "phpunit/phpunit": "~8.5 || ~9.6 || ~10.5 || ~11.5" }, "suggest": { "ext-intl": "Use Intl for transliterator_transliterate() support" @@ -9403,7 +9417,7 @@ ], "support": { "issues": "https://github.com/voku/portable-ascii/issues", - "source": "https://github.com/voku/portable-ascii/tree/2.0.3" + "source": "https://github.com/voku/portable-ascii/tree/2.1.1" }, "funding": [ { @@ -9427,7 +9441,7 @@ "type": "tidelift" } ], - "time": "2024-11-21T01:49:47+00:00" + "time": "2026-04-26T05:33:54+00:00" } ], "packages-dev": [ @@ -9693,16 +9707,16 @@ }, { "name": "hotmeteor/spectator", - "version": "v2.2.0", + "version": "v2.3.0", "source": { "type": "git", "url": "https://github.com/hotmeteor/spectator.git", - "reference": "d294ed4a08569142984155548b3e732375ce6af0" + "reference": "a903464d321a383e0346d0f7ae570c406965523f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/hotmeteor/spectator/zipball/d294ed4a08569142984155548b3e732375ce6af0", - "reference": "d294ed4a08569142984155548b3e732375ce6af0", + "url": "https://api.github.com/repos/hotmeteor/spectator/zipball/a903464d321a383e0346d0f7ae570c406965523f", + "reference": "a903464d321a383e0346d0f7ae570c406965523f", "shasum": "" }, "require": { @@ -9754,7 +9768,7 @@ ], "support": { "issues": "https://github.com/hotmeteor/spectator/issues", - "source": "https://github.com/hotmeteor/spectator/tree/v2.2.0" + "source": "https://github.com/hotmeteor/spectator/tree/v2.3.0" }, "funding": [ { @@ -9770,7 +9784,7 @@ "type": "github" } ], - "time": "2026-03-27T15:21:58+00:00" + "time": "2026-04-28T11:56:34+00:00" }, { "name": "iamcal/sql-parser", @@ -9890,16 +9904,16 @@ }, { "name": "larastan/larastan", - "version": "v3.9.5", + "version": "v3.9.6", "source": { "type": "git", "url": "https://github.com/larastan/larastan.git", - "reference": "aa637ef3c9102490e0fa9a7ea4fbdbbce4471f34" + "reference": "9ad17e83e96b63536cb6ac39c3d40d29ff9cf636" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/larastan/larastan/zipball/aa637ef3c9102490e0fa9a7ea4fbdbbce4471f34", - "reference": "aa637ef3c9102490e0fa9a7ea4fbdbbce4471f34", + "url": "https://api.github.com/repos/larastan/larastan/zipball/9ad17e83e96b63536cb6ac39c3d40d29ff9cf636", + "reference": "9ad17e83e96b63536cb6ac39c3d40d29ff9cf636", "shasum": "" }, "require": { @@ -9968,7 +9982,7 @@ ], "support": { "issues": "https://github.com/larastan/larastan/issues", - "source": "https://github.com/larastan/larastan/tree/v3.9.5" + "source": "https://github.com/larastan/larastan/tree/v3.9.6" }, "funding": [ { @@ -9976,7 +9990,7 @@ "type": "github" } ], - "time": "2026-04-11T20:10:30+00:00" + "time": "2026-04-16T10:02:43+00:00" }, { "name": "laravel/pail", @@ -10060,16 +10074,16 @@ }, { "name": "laravel/pint", - "version": "v1.29.0", + "version": "v1.29.1", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "bdec963f53172c5e36330f3a400604c69bf02d39" + "reference": "0770e9b7fafd50d4586881d456d6eb41c9247a80" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/bdec963f53172c5e36330f3a400604c69bf02d39", - "reference": "bdec963f53172c5e36330f3a400604c69bf02d39", + "url": "https://api.github.com/repos/laravel/pint/zipball/0770e9b7fafd50d4586881d456d6eb41c9247a80", + "reference": "0770e9b7fafd50d4586881d456d6eb41c9247a80", "shasum": "" }, "require": { @@ -10080,14 +10094,14 @@ "php": "^8.2.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.94.2", - "illuminate/view": "^12.54.1", - "larastan/larastan": "^3.9.3", - "laravel-zero/framework": "^12.0.5", + "friendsofphp/php-cs-fixer": "^3.95.1", + "illuminate/view": "^12.56.0", + "larastan/larastan": "^3.9.6", + "laravel-zero/framework": "^12.1.0", "mockery/mockery": "^1.6.12", "nunomaduro/termwind": "^2.4.0", "pestphp/pest": "^3.8.6", - "shipfastlabs/agent-detector": "^1.1.0" + "shipfastlabs/agent-detector": "^1.1.3" }, "bin": [ "builds/pint" @@ -10124,20 +10138,20 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2026-03-12T15:51:39+00:00" + "time": "2026-04-20T15:26:14+00:00" }, { "name": "laravel/sail", - "version": "v1.57.0", + "version": "v1.58.0", "source": { "type": "git", "url": "https://github.com/laravel/sail.git", - "reference": "fa8d057b6e9310380ccbc3a209ed7f927d54f648" + "reference": "2e5e968138ca52ed87d712449697a8364d73b466" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/sail/zipball/fa8d057b6e9310380ccbc3a209ed7f927d54f648", - "reference": "fa8d057b6e9310380ccbc3a209ed7f927d54f648", + "url": "https://api.github.com/repos/laravel/sail/zipball/2e5e968138ca52ed87d712449697a8364d73b466", + "reference": "2e5e968138ca52ed87d712449697a8364d73b466", "shasum": "" }, "require": { @@ -10187,7 +10201,7 @@ "issues": "https://github.com/laravel/sail/issues", "source": "https://github.com/laravel/sail" }, - "time": "2026-04-14T13:32:04+00:00" + "time": "2026-04-27T13:38:34+00:00" }, { "name": "marc-mabe/php-enum", @@ -10347,23 +10361,23 @@ }, { "name": "nunomaduro/collision", - "version": "v8.9.3", + "version": "v8.9.4", "source": { "type": "git", "url": "https://github.com/nunomaduro/collision.git", - "reference": "b0d8ab95b29c3189aeeb902d81215231df4c1b64" + "reference": "716af8f95a470e9094cfca09ed897b023be191a5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nunomaduro/collision/zipball/b0d8ab95b29c3189aeeb902d81215231df4c1b64", - "reference": "b0d8ab95b29c3189aeeb902d81215231df4c1b64", + "url": "https://api.github.com/repos/nunomaduro/collision/zipball/716af8f95a470e9094cfca09ed897b023be191a5", + "reference": "716af8f95a470e9094cfca09ed897b023be191a5", "shasum": "" }, "require": { "filp/whoops": "^2.18.4", "nunomaduro/termwind": "^2.4.0", "php": "^8.2.0", - "symfony/console": "^7.4.8 || ^8.0.4" + "symfony/console": "^7.4.8 || ^8.0.8" }, "conflict": { "laravel/framework": "<11.48.0 || >=14.0.0", @@ -10371,12 +10385,12 @@ }, "require-dev": { "brianium/paratest": "^7.8.5", - "larastan/larastan": "^3.9.3", - "laravel/framework": "^11.48.0 || ^12.56.0 || ^13.2.0", - "laravel/pint": "^1.29.0", - "orchestra/testbench-core": "^9.12.0 || ^10.12.1 || ^11.0.0", + "larastan/larastan": "^3.9.6", + "laravel/framework": "^11.48.0 || ^12.56.0 || ^13.5.0", + "laravel/pint": "^1.29.1", + "orchestra/testbench-core": "^9.12.0 || ^10.12.1 || ^11.2.1", "pestphp/pest": "^3.8.5 || ^4.4.3 || ^5.0.0", - "sebastian/environment": "^7.2.1 || ^8.0.4 || ^9.0.0" + "sebastian/environment": "^7.2.1 || ^8.0.4 || ^9.3.0" }, "type": "library", "extra": { @@ -10439,7 +10453,7 @@ "type": "patreon" } ], - "time": "2026-04-06T19:25:53+00:00" + "time": "2026-04-21T14:04:20+00:00" }, { "name": "opis/json-schema", @@ -10751,11 +10765,11 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.47", + "version": "2.1.53", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/79015445d8bd79e62b29140f12e5bfced1dcca65", - "reference": "79015445d8bd79e62b29140f12e5bfced1dcca65", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/ef67586798c003274797b288a68b221e4270dca7", + "reference": "ef67586798c003274797b288a68b221e4270dca7", "shasum": "" }, "require": { @@ -10800,7 +10814,7 @@ "type": "github" } ], - "time": "2026-04-13T15:49:08+00:00" + "time": "2026-04-28T16:09:00+00:00" }, { "name": "phpunit/php-code-coverage", @@ -11261,21 +11275,21 @@ }, { "name": "rector/rector", - "version": "2.4.1", + "version": "2.4.2", "source": { "type": "git", "url": "https://github.com/rectorphp/rector.git", - "reference": "000b7050b9e4fe98db2192971e56eb0b302b3feb" + "reference": "e645b6463c6a88ea5b44b17d3387d35a912c7946" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/rectorphp/rector/zipball/000b7050b9e4fe98db2192971e56eb0b302b3feb", - "reference": "000b7050b9e4fe98db2192971e56eb0b302b3feb", + "url": "https://api.github.com/repos/rectorphp/rector/zipball/e645b6463c6a88ea5b44b17d3387d35a912c7946", + "reference": "e645b6463c6a88ea5b44b17d3387d35a912c7946", "shasum": "" }, "require": { "php": "^7.4|^8.0", - "phpstan/phpstan": "^2.1.41" + "phpstan/phpstan": "^2.1.48" }, "conflict": { "rector/rector-doctrine": "*", @@ -11309,7 +11323,7 @@ ], "support": { "issues": "https://github.com/rectorphp/rector/issues", - "source": "https://github.com/rectorphp/rector/tree/2.4.1" + "source": "https://github.com/rectorphp/rector/tree/2.4.2" }, "funding": [ { @@ -11317,7 +11331,7 @@ "type": "github" } ], - "time": "2026-04-08T08:43:56+00:00" + "time": "2026-04-16T13:07:34+00:00" }, { "name": "sebastian/cli-parser", @@ -12438,27 +12452,28 @@ }, { "name": "symfony/yaml", - "version": "v8.0.8", + "version": "v7.4.8", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "54174ab48c0c0f9e21512b304be17f8150ccf8f1" + "reference": "c58fdf7b3d6c2995368264c49e4e8b05bcff2883" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/54174ab48c0c0f9e21512b304be17f8150ccf8f1", - "reference": "54174ab48c0c0f9e21512b304be17f8150ccf8f1", + "url": "https://api.github.com/repos/symfony/yaml/zipball/c58fdf7b3d6c2995368264c49e4e8b05bcff2883", + "reference": "c58fdf7b3d6c2995368264c49e4e8b05bcff2883", "shasum": "" }, "require": { - "php": ">=8.4", + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-ctype": "^1.8" }, "conflict": { - "symfony/console": "<7.4" + "symfony/console": "<6.4" }, "require-dev": { - "symfony/console": "^7.4|^8.0" + "symfony/console": "^6.4|^7.0|^8.0" }, "bin": [ "Resources/bin/yaml-lint" @@ -12489,7 +12504,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v8.0.8" + "source": "https://github.com/symfony/yaml/tree/v7.4.8" }, "funding": [ { @@ -12509,7 +12524,7 @@ "type": "tidelift" } ], - "time": "2026-03-30T15:14:47+00:00" + "time": "2026-03-24T13:12:05+00:00" }, { "name": "theseer/tokenizer", From 11a558cb665d81a5d5e6ae5677e7c69bacd72333 Mon Sep 17 00:00:00 2001 From: Dan Lemon Date: Thu, 30 Apr 2026 11:00:44 +0200 Subject: [PATCH 5/7] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- tests/Feature/Jobs/StaleLifecycleJobTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Feature/Jobs/StaleLifecycleJobTest.php b/tests/Feature/Jobs/StaleLifecycleJobTest.php index 080ba6ec..c880142f 100644 --- a/tests/Feature/Jobs/StaleLifecycleJobTest.php +++ b/tests/Feature/Jobs/StaleLifecycleJobTest.php @@ -139,7 +139,6 @@ private function createAppInstance( 'app_type' => PolydockAiApp::class, 'status' => $status, 'status_message' => $statusMessage, - 'data' => [], ]); } } From 978171417f992b6dc3f5b2ed5d0b6152a70cb420 Mon Sep 17 00:00:00 2001 From: Dan Lemon Date: Thu, 30 Apr 2026 12:45:52 +0300 Subject: [PATCH 6/7] chore: adapt --- .../BaseJob.php | 115 +++++++++--------- tests/Feature/Jobs/StaleLifecycleJobTest.php | 59 +++++++++ 2 files changed, 115 insertions(+), 59 deletions(-) diff --git a/app/Jobs/ProcessPolydockAppInstanceJobs/BaseJob.php b/app/Jobs/ProcessPolydockAppInstanceJobs/BaseJob.php index de5dded8..0c60ae79 100644 --- a/app/Jobs/ProcessPolydockAppInstanceJobs/BaseJob.php +++ b/app/Jobs/ProcessPolydockAppInstanceJobs/BaseJob.php @@ -117,110 +117,107 @@ protected function shouldSkipBecauseStatusAdvanced(PolydockAppInstanceStatus $ex } /** - * Canonical ordering of lifecycle statuses, used to detect when a queued - * job is stale because the instance has already advanced past the status - * the job was scheduled for. + * Lifecycle stage ordinals, used to detect when a queued job is stale + * because the instance has already advanced past the *stage* the job was + * scheduled for. * - * Keep this list in sync with the status flow handled by + * Each logical stage of the lifecycle gets a single ordinal. All statuses + * within the same stage (e.g. `PENDING_CREATE`, `CREATE_RUNNING`, + * `CREATE_COMPLETED`) share that ordinal, so a stale job is only + * considered "advanced past" when the instance has moved into a strictly + * later stage. In-stage progression is left alone — `WithoutOverlapping` + * handles dedup of jobs targeting the same stage. + * + * The four `RUNNING_*` statuses share a single ordinal because they are + * alternative running states rather than sequential ones. + * + * Upgrade stages sit after the running stage because an in-place upgrade + * is initiated against an already-claimed, running instance and returns + * to a running/claimed state when complete. + * + * Keep this in sync with the status flow handled by * {@see \App\PolydockEngine\Engine}, the dispatch table in * {@see \App\Listeners\ProcessPolydockAppInstanceStatusChange}, and the * stage groupings on {@see \App\Models\PolydockAppInstance}. - * - * Upgrade statuses sit after the running/claimed states because an - * in-place upgrade is initiated against an already-claimed, running - * instance and returns to a running/claimed state when complete. - * - * @return list */ - private static function lifecycleStatusOrder(): array + private static function lifecycleStageOrdinal(PolydockAppInstanceStatus $status): ?int { - return [ - // New / pre-create - PolydockAppInstanceStatus::NEW, + return match ($status) { + PolydockAppInstanceStatus::NEW => 0, + PolydockAppInstanceStatus::PENDING_PRE_CREATE, PolydockAppInstanceStatus::PRE_CREATE_RUNNING, - PolydockAppInstanceStatus::PRE_CREATE_COMPLETED, - // Create + PolydockAppInstanceStatus::PRE_CREATE_COMPLETED => 10, + PolydockAppInstanceStatus::PENDING_CREATE, PolydockAppInstanceStatus::CREATE_RUNNING, - PolydockAppInstanceStatus::CREATE_COMPLETED, - // Post-create + PolydockAppInstanceStatus::CREATE_COMPLETED => 20, + PolydockAppInstanceStatus::PENDING_POST_CREATE, PolydockAppInstanceStatus::POST_CREATE_RUNNING, - PolydockAppInstanceStatus::POST_CREATE_COMPLETED, - // Pre-deploy + PolydockAppInstanceStatus::POST_CREATE_COMPLETED => 30, + PolydockAppInstanceStatus::PENDING_PRE_DEPLOY, PolydockAppInstanceStatus::PRE_DEPLOY_RUNNING, - PolydockAppInstanceStatus::PRE_DEPLOY_COMPLETED, - // Deploy + PolydockAppInstanceStatus::PRE_DEPLOY_COMPLETED => 40, + PolydockAppInstanceStatus::PENDING_DEPLOY, PolydockAppInstanceStatus::DEPLOY_RUNNING, - PolydockAppInstanceStatus::DEPLOY_COMPLETED, - // Post-deploy + PolydockAppInstanceStatus::DEPLOY_COMPLETED => 50, + PolydockAppInstanceStatus::PENDING_POST_DEPLOY, PolydockAppInstanceStatus::POST_DEPLOY_RUNNING, - PolydockAppInstanceStatus::POST_DEPLOY_COMPLETED, - // Claim + PolydockAppInstanceStatus::POST_DEPLOY_COMPLETED => 60, + PolydockAppInstanceStatus::PENDING_POLYDOCK_CLAIM, PolydockAppInstanceStatus::POLYDOCK_CLAIM_RUNNING, - PolydockAppInstanceStatus::POLYDOCK_CLAIM_COMPLETED, - // Running + PolydockAppInstanceStatus::POLYDOCK_CLAIM_COMPLETED => 70, + PolydockAppInstanceStatus::RUNNING_HEALTHY_UNCLAIMED, PolydockAppInstanceStatus::RUNNING_HEALTHY_CLAIMED, PolydockAppInstanceStatus::RUNNING_UNHEALTHY, - PolydockAppInstanceStatus::RUNNING_UNRESPONSIVE, - // Pre-upgrade (in-place upgrade against a running, claimed instance) + PolydockAppInstanceStatus::RUNNING_UNRESPONSIVE => 80, + PolydockAppInstanceStatus::PENDING_PRE_UPGRADE, PolydockAppInstanceStatus::PRE_UPGRADE_RUNNING, - PolydockAppInstanceStatus::PRE_UPGRADE_COMPLETED, - // Upgrade + PolydockAppInstanceStatus::PRE_UPGRADE_COMPLETED => 90, + PolydockAppInstanceStatus::PENDING_UPGRADE, PolydockAppInstanceStatus::UPGRADE_RUNNING, - PolydockAppInstanceStatus::UPGRADE_COMPLETED, - // Post-upgrade + PolydockAppInstanceStatus::UPGRADE_COMPLETED => 100, + PolydockAppInstanceStatus::PENDING_POST_UPGRADE, PolydockAppInstanceStatus::POST_UPGRADE_RUNNING, - PolydockAppInstanceStatus::POST_UPGRADE_COMPLETED, - // Pre-remove + PolydockAppInstanceStatus::POST_UPGRADE_COMPLETED => 110, + PolydockAppInstanceStatus::PENDING_PRE_REMOVE, PolydockAppInstanceStatus::PRE_REMOVE_RUNNING, - PolydockAppInstanceStatus::PRE_REMOVE_COMPLETED, - // Remove + PolydockAppInstanceStatus::PRE_REMOVE_COMPLETED => 120, + PolydockAppInstanceStatus::PENDING_REMOVE, PolydockAppInstanceStatus::REMOVE_RUNNING, - PolydockAppInstanceStatus::REMOVE_COMPLETED, - // Post-remove + PolydockAppInstanceStatus::REMOVE_COMPLETED => 130, + PolydockAppInstanceStatus::PENDING_POST_REMOVE, PolydockAppInstanceStatus::POST_REMOVE_RUNNING, - PolydockAppInstanceStatus::POST_REMOVE_COMPLETED, - PolydockAppInstanceStatus::REMOVED, - ]; - } - - private static function lifecycleStatusIndexMap(): array - { - static $statusIndexMap = null; + PolydockAppInstanceStatus::POST_REMOVE_COMPLETED => 140, - if ($statusIndexMap === null) { - $statusIndexMap = []; - - foreach (self::lifecycleStatusOrder() as $index => $status) { - $statusIndexMap[$status->value] = $index; - } - } + PolydockAppInstanceStatus::REMOVED => 150, - return $statusIndexMap; + default => null, + }; } private function isKnownStatusProgression(PolydockAppInstanceStatus $expectedStatus, PolydockAppInstanceStatus $currentStatus): bool { - $statusIndexMap = self::lifecycleStatusIndexMap(); + $expectedOrdinal = self::lifecycleStageOrdinal($expectedStatus); + $currentOrdinal = self::lifecycleStageOrdinal($currentStatus); - if (! array_key_exists($expectedStatus->value, $statusIndexMap) || ! array_key_exists($currentStatus->value, $statusIndexMap)) { + if ($expectedOrdinal === null || $currentOrdinal === null) { return false; } - return $statusIndexMap[$currentStatus->value] > $statusIndexMap[$expectedStatus->value]; + return $currentOrdinal > $expectedOrdinal; } public function polydockJobStart() diff --git a/tests/Feature/Jobs/StaleLifecycleJobTest.php b/tests/Feature/Jobs/StaleLifecycleJobTest.php index c880142f..7117f462 100644 --- a/tests/Feature/Jobs/StaleLifecycleJobTest.php +++ b/tests/Feature/Jobs/StaleLifecycleJobTest.php @@ -121,6 +121,65 @@ public function test_create_job_throws_status_flow_exception_when_status_is_not_ (new CreateJob($appInstance->id))->handle(); } + public function test_create_job_does_not_skip_when_instance_is_in_the_same_create_stage(): void + { + // Stages are per-phase: PENDING_CREATE, CREATE_RUNNING, and + // CREATE_COMPLETED all live in the "create" stage. A stale job whose + // expected status is PENDING_CREATE must NOT be silently skipped just + // because the instance has progressed to CREATE_RUNNING / COMPLETED + // within the same stage. (Dedup is the responsibility of + // WithoutOverlapping, not the skip logic.) + $appInstance = $this->createAppInstance( + 'same-stage-create-job', + PolydockAppInstanceStatus::CREATE_COMPLETED, + 'Already completed create', + ); + + $this->expectException(PolydockAppInstanceStatusFlowException::class); + + (new CreateJob($appInstance->id))->handle(); + } + + public function test_claim_job_does_not_skip_when_instance_is_in_a_sibling_running_state(): void + { + // All RUNNING_* statuses share a single stage ordinal because they + // are alternative running states, not sequential ones. A stale + // ClaimJob landing on RUNNING_HEALTHY_UNCLAIMED is still relevant — + // the instance has not actually advanced past the claim stage into + // a later phase like upgrade or remove. + $appInstance = $this->createAppInstance( + 'sibling-running-claim-job', + PolydockAppInstanceStatus::RUNNING_HEALTHY_UNCLAIMED, + 'Running unclaimed, awaiting claim', + ); + + // ClaimJob's expected status is PENDING_POLYDOCK_CLAIM (claim stage). + // RUNNING_HEALTHY_UNCLAIMED is in the running stage, which is + // strictly after claim, so this *should* skip. + (new ClaimJob($appInstance->id))->handle(); + + $appInstance->refresh(); + + $this->assertSame(PolydockAppInstanceStatus::RUNNING_HEALTHY_UNCLAIMED, $appInstance->status); + $this->assertSame('Running unclaimed, awaiting claim', $appInstance->status_message); + } + + public function test_deploy_job_does_not_skip_when_instance_is_in_the_same_deploy_stage(): void + { + // PENDING_DEPLOY, DEPLOY_RUNNING, DEPLOY_COMPLETED all share the + // deploy stage. A stale DeployJob targeting PENDING_DEPLOY must not + // be silently skipped when the instance is at DEPLOY_RUNNING. + $appInstance = $this->createAppInstance( + 'same-stage-deploy-job', + PolydockAppInstanceStatus::DEPLOY_RUNNING, + 'Deploy in progress', + ); + + $this->expectException(PolydockAppInstanceStatusFlowException::class); + + (new DeployJob($appInstance->id))->handle(); + } + private function createAppInstance( string $name, PolydockAppInstanceStatus $status, From 5c115391574cd09a143c62400599346512835aea Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Apr 2026 10:05:58 +0000 Subject: [PATCH 7/7] fix: rename misleading test method and fix contradictory comment Agent-Logs-Url: https://github.com/amazeeio/polydock-engine/sessions/9de00697-ca6a-47fd-aa5c-c78e1dde1814 Co-authored-by: dan2k3k4 <158704+dan2k3k4@users.noreply.github.com> --- tests/Feature/Jobs/StaleLifecycleJobTest.php | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/tests/Feature/Jobs/StaleLifecycleJobTest.php b/tests/Feature/Jobs/StaleLifecycleJobTest.php index 7117f462..44b4dc35 100644 --- a/tests/Feature/Jobs/StaleLifecycleJobTest.php +++ b/tests/Feature/Jobs/StaleLifecycleJobTest.php @@ -140,22 +140,19 @@ public function test_create_job_does_not_skip_when_instance_is_in_the_same_creat (new CreateJob($appInstance->id))->handle(); } - public function test_claim_job_does_not_skip_when_instance_is_in_a_sibling_running_state(): void + public function test_claim_job_skips_when_instance_is_in_a_sibling_running_state(): void { - // All RUNNING_* statuses share a single stage ordinal because they - // are alternative running states, not sequential ones. A stale - // ClaimJob landing on RUNNING_HEALTHY_UNCLAIMED is still relevant — - // the instance has not actually advanced past the claim stage into - // a later phase like upgrade or remove. + // ClaimJob's expected status is PENDING_POLYDOCK_CLAIM. All + // RUNNING_* statuses sit strictly after the claim stage in the + // lifecycle order, so a stale ClaimJob must be skipped even when + // the instance has only advanced to RUNNING_HEALTHY_UNCLAIMED + // rather than a later phase like upgrade or remove. $appInstance = $this->createAppInstance( 'sibling-running-claim-job', PolydockAppInstanceStatus::RUNNING_HEALTHY_UNCLAIMED, 'Running unclaimed, awaiting claim', ); - // ClaimJob's expected status is PENDING_POLYDOCK_CLAIM (claim stage). - // RUNNING_HEALTHY_UNCLAIMED is in the running stage, which is - // strictly after claim, so this *should* skip. (new ClaimJob($appInstance->id))->handle(); $appInstance->refresh();