toContain(HasRestartLimit::class); $resource = new $modelClass; expect($resource->getFillable())->toContain( 'restart_count', 'max_restart_count', 'restart_limit_reached', 'last_restart_at', 'last_restart_type', )->and($resource->getCasts())->toMatchArray([ 'restart_count' => 'integer', 'max_restart_count' => 'integer', 'restart_limit_reached' => 'boolean', 'last_restart_at' => 'datetime', ]); })->with([ ApplicationPreview::class, ServiceApplication::class, ServiceDatabase::class, StandaloneClickhouse::class, StandaloneDragonfly::class, StandaloneKeydb::class, StandaloneMariadb::class, StandaloneMongodb::class, StandaloneMysql::class, StandalonePostgresql::class, StandaloneRedis::class, ]); it('collects restart counts for preview and service containers from both status sources', function () { $dockerStatus = file_get_contents(app_path('Actions/Docker/GetContainersStatus.php')); $sentinelStatus = file_get_contents(app_path('Jobs/PushServerUpdateJob.php')); expect($dockerStatus) ->toContain('previewContainerRestartCounts') ->toContain('serviceContainerRestartCounts') ->and($sentinelStatus) ->toContain('previewContainerRestartCounts') ->toContain('serviceContainerRestartCounts'); }); it('ignores docker compose one-off job containers in both status sources', function () { $dockerStatus = file_get_contents(app_path('Actions/Docker/GetContainersStatus.php')); $sentinelStatus = file_get_contents(app_path('Jobs/PushServerUpdateJob.php')); expect($dockerStatus) ->toContain("filter_var(data_get(\$labels, 'com.docker.compose.oneoff'), FILTER_VALIDATE_BOOLEAN)") ->and($sentinelStatus) ->toContain("filter_var(\$labels->get('com.docker.compose.oneoff'), FILTER_VALIDATE_BOOLEAN)"); }); it('shows restart limit warnings for every resource family', function () { $previews = file_get_contents(resource_path('views/livewire/project/application/previews.blade.php')); $serviceCard = file_get_contents(resource_path('views/livewire/project/service/resource-card.blade.php')); $applicationStatus = file_get_contents(resource_path('views/livewire/project/application/status.blade.php')); $databaseStatus = file_get_contents(resource_path('views/livewire/project/database/status.blade.php')); expect($previews)->toContain('') ->and($serviceCard)->toContain('') ->and($applicationStatus)->toContain('') ->and($databaseStatus)->toContain(''); $serviceStatus = file_get_contents(resource_path('views/livewire/project/service/status.blade.php')); $serviceHeading = file_get_contents(resource_path('views/livewire/project/service/heading.blade.php')); expect($serviceStatus)->toContain('') ->and($serviceStatus)->toContain('$selectedResource?->status ?? $service->status') ->and($serviceHeading)->toContain('') ->and($serviceHeading)->toContain('$selectedResource?->status ?? $service->status'); }); it('matches application restart badge layout on mobile resource headings', function () { $applicationHeading = file_get_contents(resource_path('views/livewire/project/application/heading.blade.php')); $databaseHeading = file_get_contents(resource_path('views/livewire/project/database/heading.blade.php')); $serviceHeading = file_get_contents(resource_path('views/livewire/project/service/heading.blade.php')); foreach ([$applicationHeading, $databaseHeading, $serviceHeading] as $heading) { expect($heading) ->toContain('class="relative flex w-full min-w-0 items-center gap-2"') ->toContain('class="flex w-full flex-wrap gap-1"'); } }); it('shows database restart limits in shared resource listings', function () { $resourceIndex = file_get_contents(app_path('Livewire/Project/Resource/Index.php')); $serverResources = file_get_contents(resource_path('views/livewire/server/resources.blade.php')); $destination = file_get_contents(resource_path('views/livewire/project/shared/destination.blade.php')); expect($resourceIndex) ->toContain("method_exists(\$item, 'stoppedAfterRestartLimit')") ->not->toContain("\$type === 'application' && \$item->stoppedAfterRestartLimit()") ->and($serverResources) ->toContain('') ->and($destination) ->toContain(''); }); it('keeps the service resource table readable with horizontal scrolling on mobile', function () { $configuration = file_get_contents(resource_path('views/livewire/project/service/configuration.blade.php')); $resourceCard = file_get_contents(resource_path('views/livewire/project/service/resource-card.blade.php')); expect($configuration) ->toContain("'overflow-x-auto rounded-xl") ->toContain('min-w-[48rem]') ->and($resourceCard) ->toContain('min-w-[48rem]') ->not->toContain('