create([ 'name' => 'postgres', 'category' => ServiceCategory::DATABASE, 'type' => ServiceType::POSTGRES, 'version' => '18', 'version_track' => '18', 'driver_name' => 'postgres.18', 'current_image_digest' => 'sha256:old', 'config' => [ 'backup_enabled' => true, 'backup_command' => 'pg_dump --format=custom keystone > /home/keystone/backups/pre-update.dump', ], ]); $operation = app(CreateStatefulServiceUpdateOperation::class)->execute( service: $service, imageDigest: 'sha256:new', backupRequested: true, ); expect($service->refresh()->available_image_digest)->toBe('sha256:new') ->and($service->update_status)->toBe('update_pending') ->and($operation->steps()->pluck('name')->all()) ->toBe([ 'Acknowledge downtime and data risk', 'Run pre-update backup', 'Render compose with updated image digest', 'Stop existing container', 'Preserve named volume', 'Start service with updated image digest', 'Health check updated service', ]) ->and($operation->steps()->where('name', 'Run pre-update backup')->first()->script) ->toBe('pg_dump --format=custom keystone > /home/keystone/backups/pre-update.dump') ->and($operation->steps()->where('name', 'Render compose with updated image digest')->first()->script) ->toContain('base64 -d') ->and($operation->steps()->where('name', 'Stop existing container')->first()->script) ->toBe("docker compose -f /home/keystone/services/{$service->id}/compose.yml stop {$service->name}") ->and($operation->steps()->where('name', 'Preserve named volume')->first()->script) ->toBe("docker volume inspect keystone_service_{$service->id}_postgres_data >/dev/null") ->and($operation->steps()->where('name', 'Health check updated service')->first()->script) ->toContain('docker inspect --format'); }); it('rejects backup requests when no backup capability is configured', function () { $service = Service::factory()->create([ 'category' => ServiceCategory::DATABASE, 'type' => ServiceType::POSTGRES, 'version' => '18', 'version_track' => '18', 'driver_name' => 'postgres.18', 'config' => [ 'backup_enabled' => false, ], ]); expect(fn () => app(CreateStatefulServiceUpdateOperation::class)->execute($service, 'sha256:new', backupRequested: true)) ->toThrow(InvalidArgumentException::class, 'Backups are not configured for this service.'); }); it('rejects stateful update operations for stateless laravel services', function () { $service = Service::factory()->create([ 'category' => ServiceCategory::APPLICATION, 'type' => ServiceType::LARAVEL, 'version' => 'php-8.4', 'version_track' => 'php-8.4', 'driver_name' => 'laravel.php-8.4', ]); expect(fn () => app(CreateStatefulServiceUpdateOperation::class)->execute($service, 'sha256:new')) ->toThrow(InvalidArgumentException::class, 'Only Postgres and Valkey have v1 stateful update operations.'); });