0, 'fqdn' => 'https://coolify.test']); }); function applicationWithRestartState(array $attributes = []): Application { $application = new Application; $application->forceFill(array_merge([ 'status' => 'exited:unhealthy', 'container_present' => true, 'restart_count' => 2, 'max_restart_count' => 2, 'restart_limit_reached' => true, 'last_restart_type' => 'crash', 'last_restart_at' => now(), ], $attributes)); return $application; } it('detects applications stopped after reaching the crash restart limit', function () { expect(applicationWithRestartState()->stoppedAfterRestartLimit())->toBeTrue() ->and(applicationWithRestartState(['status' => 'running:unhealthy'])->stoppedAfterRestartLimit())->toBeFalse() ->and(applicationWithRestartState(['restart_limit_reached' => false])->stoppedAfterRestartLimit())->toBeFalse(); }); it('keeps the restart limit state after Docker resets its counter', function () { expect(applicationWithRestartState([ 'restart_count' => 0, 'last_restart_type' => null, 'last_restart_at' => null, ])->stoppedAfterRestartLimit())->toBeTrue(); }); it('preserves exited application state when the container snapshot is empty', function () { $application = Mockery::mock(Application::class)->makePartial(); $application->setRelation('additional_servers', collect()); $application->forceFill([ 'id' => 1, 'status' => 'exited:unhealthy', 'container_present' => true, 'restart_limit_reached' => true, ]); $application->shouldNotReceive('update'); $services = Mockery::mock(); $services->shouldReceive('get')->once()->andReturn(collect()); $server = Mockery::mock(Server::class, function (MockInterface $mock) use ($application, $services) { $mock->shouldReceive('isFunctional')->once()->andReturnTrue(); $mock->shouldReceive('applications')->once()->andReturn(collect([$application])); $mock->shouldReceive('databases')->once()->andReturn(collect()); $mock->shouldReceive('services')->once()->andReturn($services); $mock->shouldReceive('previews')->once()->andReturn(collect()); })->makePartial(); $server->setRelation('team', (object) ['id' => 1]); GetContainersStatus::run($server, collect(), collect()); expect($application->container_present)->toBeTrue() ->and($application->restart_limit_reached)->toBeTrue(); }); it('does not infer the restart limit from an exited existing container', function () { expect(applicationWithRestartState([ 'container_present' => true, 'restart_limit_reached' => false, ])->stoppedAfterRestartLimit())->toBeFalse(); }); it('shows a stopped after restart limit warning badge', function () { $html = view('components.application.restart-limit-warning', [ 'application' => applicationWithRestartState(), ])->render(); expect($html)->toContain('Restart limit reached') ->not->toContain('Stopped after reaching restart limit (2/2).') ->and($html)->toContain('Container has crashed and Coolify stopped it after 2 restart attempts.'); }); it('does not show the restart limit warning for a normal manual stop', function () { $html = view('components.application.restart-limit-warning', [ 'application' => applicationWithRestartState([ 'restart_count' => 0, 'last_restart_type' => null, 'restart_limit_reached' => false, ]), ])->render(); expect($html)->not->toContain('Restart limit reached'); }); it('builds restart limit notification urls from the instance base url', function () { $application = new Application; $application->forceFill([ 'name' => 'crashy-app', 'uuid' => 'application-uuid', 'restart_count' => 2, 'max_restart_count' => 2, ]); $application->setRelation('environment', (object) [ 'uuid' => 'environment-uuid', 'name' => 'production', 'project' => (object) ['uuid' => 'project-uuid'], ]); $notification = new RestartLimitReached($application); expect($notification->resource_url)->toBe('https://coolify.test/project/project-uuid/environment/environment-uuid/application/application-uuid'); }); it('links preview, service resource and database restart limit notifications to their pages', function () { $environment = (object) ['uuid' => 'environment-uuid', 'name' => 'production', 'project' => (object) ['uuid' => 'project-uuid']]; $application = new Application; $application->forceFill(['name' => 'app', 'uuid' => 'application-uuid']); $application->setRelation('environment', $environment); $preview = new ApplicationPreview; $preview->forceFill(['uuid' => 'preview-uuid', 'pull_request_id' => 42, 'restart_count' => 2, 'max_restart_count' => 2]); $preview->setRelation('application', $application); $service = new Service; $service->forceFill(['uuid' => 'service-uuid']); $service->setRelation('environment', $environment); $serviceApplication = new ServiceApplication; $serviceApplication->forceFill(['name' => 'database', 'uuid' => 'service-application-uuid', 'restart_count' => 2, 'max_restart_count' => 2]); $serviceApplication->setRelation('service', $service); $database = new StandalonePostgresql; $database->forceFill(['name' => 'postgres', 'uuid' => 'database-uuid', 'restart_count' => 2, 'max_restart_count' => 2]); $database->setRelation('environment', $environment); expect((new RestartLimitReached($preview))->resource_url)->toBe('https://coolify.test/project/project-uuid/environment/environment-uuid/application/application-uuid') ->and((new RestartLimitReached($serviceApplication))->resource_url)->toBe('https://coolify.test/project/project-uuid/environment/environment-uuid/service/service-uuid') ->and((new RestartLimitReached($database))->resource_url)->toBe('https://coolify.test/project/project-uuid/environment/environment-uuid/database/database-uuid'); }); it('uses the resolved environment project name in Slack restart limit notifications', function () { $environment = (object) [ 'uuid' => 'environment-uuid', 'name' => 'production', 'project' => (object) [ 'uuid' => 'project-uuid', 'name' => 'Coolify', ], ]; $application = new class extends Application { public function link(): string { return 'https://coolify.test/application'; } }; $application->forceFill(['name' => 'app']); $application->setRelation('environment', $environment); $preview = new ApplicationPreview; $preview->forceFill([ 'uuid' => 'preview-uuid', 'pull_request_id' => 42, 'restart_count' => 2, 'max_restart_count' => 2, ]); $preview->setRelation('application', $application); $serviceResource = new class extends BaseModel {}; $serviceResource->forceFill([ 'name' => 'database', 'uuid' => 'service-resource-uuid', 'restart_count' => 2, 'max_restart_count' => 2, ]); $serviceResource->setRelation('service', new class($environment) { public function __construct(public object $environment) {} public function link(): string { return 'https://coolify.test/service'; } }); expect((new RestartLimitReached($preview))->toSlack()->description) ->toContain('*Project:* Coolify') ->and((new RestartLimitReached($serviceResource))->toSlack()->description) ->toContain('*Project:* Coolify'); });