Files
api/services/nginx/app/tests/Unit/Cron/CronWorkerWiringTest.php
T
Jeppe B 8d8f0eccce Fix cron worker minute cadence (#324)
Replace drifting cron loops with persistent cadence-tracked workers and CI-verifiable Compose wiring.
2026-07-27 18:16:09 +02:00

107 lines
5.0 KiB
PHP

<?php
$cronAppRoot = dirname(__DIR__, 3);
require_once $cronAppRoot . '/classes/cron_worker.php';
it('wires cron workers through schema, CLI, scheduler, and superuser routes', function (): void {
$appRoot = dirname(__DIR__, 3);
$repoRoot = getenv('PLENO_REPO_ROOT_FOR_TESTS') ?: dirname($appRoot, 3);
$schema = file_get_contents($appRoot . '/classes/cron_schema_bootstrap.php');
$worker = file_get_contents($appRoot . '/classes/cron_worker.php');
$scheduler = file_get_contents($appRoot . '/classes/cron_scheduler.php');
$cli = file_get_contents($appRoot . '/cli.php');
$route = file_get_contents($appRoot . '/routes/cronRoute.php');
$manager = file_get_contents($appRoot . '/classes/release_manager.php');
$composeFiles = [
$repoRoot . '/docker-compose.yml',
$repoRoot . '/docker-compose.example.yml',
$repoRoot . '/docker-compose.prod.standalone.yml',
];
expect($schema)->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('/superuser/cron/workers/deploy');
expect($route)->toContain('$response->success($result, 202)');
expect($route)->toContain('queueTaskRun(');
expect($route)->toContain('$response->success($run, 202)');
expect($route)->toContain('superuser_cron_view');
expect($route)->toContain('superuser_cron_manage');
expect($route)->toContain('superuser_coolify_manage');
expect($manager)->toContain("private const CRON_WORKER_APP = 'cron'");
expect($manager)->toContain("private const CRON_WORKER_START_COMMAND = 'php index.php run cron-worker'");
expect($manager)->toContain('deployCronWorkerAfterApiDeployment');
expect($manager)->toContain('deployment_kind = \'cron_worker\'');
expect($manager)->toContain('createCronWorkerDeploymentRecord');
expect($manager)->toContain('waiting_for_heartbeat');
expect($manager)->toContain('cronWorkerAutoprovisionRequired');
expect($manager)->toContain('cron_worker_autoprovision_disabled');
expect($manager)->toContain('cron_worker_deploy_failed');
expect($manager)->toContain('auto_deploy = 0');
foreach ($composeFiles as $composeFile) {
$compose = file_get_contents($composeFile);
expect(str_contains($compose, 'command: ["php", "index.php", "run", "cron-worker"]'))->toBeTrue();
expect(str_contains($compose, 'while true; do php index.php run cron; sleep 60; done'))->toBeFalse();
}
});
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();
});