## Summary The Coolify-based auto-deployment of a separate `cron` worker app after every API deploy was never reliable. This PR removes the ~800 lines of dead auto-deploy logic from `release_manager.php` while keeping the underlying cron mechanism (`cron_worker.php`, `cron_scheduler.php`, the docker-compose `cron-worker` service) intact. ## Changes - **`release_manager.php`** (-818 lines) - Removed 19 private methods: `deployCronWorker*`, `cronWorker*`, `cronWorkerAutoprovision*`, etc. - Removed 3 constants: `CRON_WORKER_APP`, `CRON_WORKER_START_COMMAND`, `CRON_WORKER_DESIRED_COUNT` - Kept `cronWorkerStatus()` but rewrote as a direct DB query (no Coolify dependency) - **`tests/Unit/ReleaseManager/ReleaseManagerTest.php`** (-208 lines, removed 9 cron-worker tests) - **`tests/Unit/Cron/CronWorkerWiringTest.php`** (rewritten — now asserts removed wiring is GONE) - **`docs/CRON_PLAN.md`** (new — comprehensive plan) ## What replaced the broken auto-deploy - The cron worker runs as part of the main API docker-compose stack (the `cron-worker` service is unchanged) - New verification cron `1bb56ba8-2f3e-4bea-baa2-39801ea88ea8` runs `/workspace/scripts/verify-api-cron.py` every 5 min - Alerts to Slack #ai-daily (`C0AM3E43249`) if no fresh heartbeat in 10+ min ## Test results - 4/4 cron tests pass - 54/54 ReleaseManager tests pass - Full Unit suite: **1279 passed** (same 7 pre-existing failures on master, unchanged) - `php -l` passes on all modified files ## Plan See `docs/CRON_PLAN.md` for the full audit, plan, and acceptance criteria. 🤖 Generated with [OpenClaw](https://docs.openclaw.ai) --------- Co-authored-by: bugfix <bugfix@truckwash.local> Co-authored-by: openhands <openhands@all-hands.dev>
116 lines
5.4 KiB
PHP
116 lines
5.4 KiB
PHP
<?php
|
|
|
|
// Test that the cron mechanism is properly wired. The Coolify auto-deploy logic
|
|
// was removed from release_manager.php 2026-08-17, so this test no longer asserts
|
|
// anything about cron worker deployment. The actual cron mechanism
|
|
// (cron_worker.php, cron_scheduler.php, cli.php, cronRoute.php) is unchanged.
|
|
|
|
$cronAppRoot = dirname(__DIR__, 3);
|
|
require_once $cronAppRoot . '/classes/cron_worker.php';
|
|
|
|
it('wires cron workers through schema, scheduler, CLI, and 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');
|
|
|
|
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('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");
|
|
});
|