toContain('CREATE TABLE IF NOT EXISTS cron_worker_state'); expect($schema)->toContain('last_heartbeat_at'); expect($schema)->toContain('last_stale_run_count'); expect($schema)->toContain('force_run TINYINT(1) NOT NULL DEFAULT 0'); expect($worker)->toContain('CRON_WORKER_POLL_SECONDS'); expect($worker)->toContain('CRON_WORKER_HEARTBEAT_SECONDS'); expect($worker)->toContain('CRON_WORKER_RELEASE_TARGET_ID'); expect($worker)->toContain('markExpiredRunningRuns'); expect($worker)->toContain('runDue($this->source)'); expect($worker)->toContain('$pollStarted + $this->poll_seconds'); expect($worker)->toContain("'minute_cadence' => ["); expect($worker)->toContain("'maximum_gap_seconds' => 60"); expect($scheduler)->toContain('function markExpiredRunningRuns'); expect($scheduler)->toContain('function queueTaskRun'); expect($scheduler)->toContain('function queuedRuns'); expect($scheduler)->toContain('function runQueuedRun'); expect($scheduler)->toContain("WHERE status = 'queued'"); expect($scheduler)->toContain("SET status = 'running'"); expect($scheduler)->toContain("r.status = 'timed_out'"); expect($scheduler)->toContain('s.current_run_id = NULL'); expect($scheduler)->toContain('$this->executeClaimedRun($definition, $run_id, $started, $scheduled_for)'); expect($scheduler)->toContain('$scheduleAnchor = $scheduled_for'); expect($cli)->toContain("case 'cron-worker'"); expect($cli)->toContain('new \\classes\\cron_worker()'); expect($route)->toContain('/superuser/cron/workers'); expect($route)->toContain('queueTaskRun('); expect($route)->toContain('$response->success($run, 202)'); expect($route)->toContain('superuser_cron_view'); expect($route)->toContain('superuser_cron_manage'); }); it('starts the cron-worker service via the docker-compose entrypoint', function (): void { $appRoot = dirname(__DIR__, 3); $repoRoot = getenv('PLENO_REPO_ROOT_FOR_TESTS') ?: dirname($appRoot, 3); $composeFiles = [ $repoRoot . '/docker-compose.yml', $repoRoot . '/docker-compose.example.yml', $repoRoot . '/docker-compose.prod.standalone.yml', ]; foreach ($composeFiles as $composeFile) { $compose = file_get_contents($composeFile); expect(str_contains($compose, 'command: ["php", "index.php", "run", "cron-worker"]'))->toBeTrue(); } }); it('reports consecutive scheduler loops as once-per-minute execution proof', function (): void { $reflection = new ReflectionClass(\classes\cron_worker::class); $worker = $reflection->newInstanceWithoutConstructor(); $publicWorker = $reflection->getMethod('publicWorker'); $now = time(); $row = [ 'worker_id' => 'proof-worker', 'name' => 'Proof worker', 'source' => 'test', 'status' => 'running', 'poll_seconds' => 15, 'last_heartbeat_at' => date('Y-m-d H:i:s', $now - 5), 'last_loop_started_at' => date('Y-m-d H:i:s', $now - 10), 'last_loop_finished_at' => date('Y-m-d H:i:s', $now - 9), 'last_loop_gap_seconds' => 15, 'consecutive_minute_loops' => 4, ]; $result = $publicWorker->invoke($worker, $row); expect($result['minute_cadence'])->toBe([ 'verified' => true, 'maximum_gap_seconds' => 60, 'last_gap_seconds' => 15, 'consecutive_loops' => 4, ]); $row['last_loop_gap_seconds'] = 61; $result = $publicWorker->invoke($worker, $row); expect($result['minute_cadence']['verified'])->toBeFalse(); }); it('exposes a cron status endpoint that no longer references Coolify auto-deploy', function (): void { $appRoot = dirname(__DIR__, 3); $manager = file_get_contents($appRoot . '/classes/release_manager.php'); // The Coolify auto-deploy constants and methods must be gone expect($manager)->not->toContain("private const CRON_WORKER_APP = 'cron'"); expect($manager)->not->toContain("private const CRON_WORKER_START_COMMAND = 'php index.php run cron-worker'"); expect($manager)->not->toContain('function deployCronWorker'); expect($manager)->not->toContain('function deployCronWorkerAfterApiDeployment'); expect($manager)->not->toContain('function cronWorkerAutoprovision'); expect($manager)->not->toContain('function cronWorkerHealth'); // The cronWorkerStatus method should still exist as a thin DB wrapper expect($manager)->toContain('public function cronWorkerStatus'); expect($manager)->toContain("'coolify_auto_deploy_enabled' => false"); });