firstWhere('key', $key); } /** * „Ohne Worker passiert schlicht nichts, ohne Fehlermeldung." Genau dieser * Zustand war in der Konsole bisher nicht von „alles ruhig" zu unterscheiden. * * Zwei getrennte Herzschläge, weil Zeitplaner und Warteschlangen-Worker * getrennt ausfallen: der Zeitplaner kann laufen und Aufträge einstellen, die * niemand abholt. */ it('reports a missing heartbeat as not satisfied', function () { expect(operationCheck('operation.scheduler')->satisfied)->toBeFalse(); }); it('accepts a fresh heartbeat', function () { Settings::set('heartbeat.scheduler', now()->toIso8601String()); expect(operationCheck('operation.scheduler')->satisfied)->toBeTrue(); }); it('rejects a heartbeat older than five minutes', function () { Settings::set('heartbeat.scheduler', now()->subMinutes(6)->toIso8601String()); expect(operationCheck('operation.scheduler')->satisfied)->toBeFalse(); }); it('proves the worker, not just the scheduler', function () { // Der Zeitplaner läuft, der Bereitstellungs-Worker nicht: der eine // Herzschlag ist frisch, der andere nicht. Settings::set('heartbeat.scheduler', now()->toIso8601String()); expect(operationCheck('operation.scheduler')->satisfied)->toBeTrue(); expect(operationCheck('operation.queue_provisioning')->satisfied)->toBeFalse(); }); it('records the worker heartbeat when the job runs', function () { (new RecordProvisioningHeartbeat)->handle(); expect(operationCheck('operation.queue_provisioning')->satisfied)->toBeTrue(); }); /** * Fix-Runde (Befund 1): calling ->handle() directly, as every test above * does, proves only that the job writes its value — never that it is the * PROVISIONING worker doing it. That one property is the entire reason a * second heartbeat exists at all: if this job ever migrated onto the default * queue, this check would keep reporting a live provisioning worker while the * queue that actually matters sat unattended — the exact silent failure this * task was built to catch. */ it('is pushed onto the provisioning queue, not the default one', function () { Queue::fake(); RecordProvisioningHeartbeat::dispatch(); Queue::assertPushedOn('provisioning', RecordProvisioningHeartbeat::class); }); /** * Fix-Runde (Befund 2): reproduced live by the reviewer — a garbled value in * the heartbeat row made Carbon::parse() throw, which propagated straight out * of Readiness::all(). Not just this one check failing: the whole readiness * page, whose only purpose is to calmly report what is missing, going down * with it. Unreachable through any real write path today, which is the * argument FOR guarding it cheaply, not against. */ it('treats an unreadable heartbeat as not satisfied, without taking the whole page down', function () { Settings::set('heartbeat.scheduler', 'not-a-timestamp'); $checks = Readiness::all(); expect(collect($checks)->firstWhere('key', 'operation.scheduler')->satisfied)->toBeFalse(); }); /** * Fix-Runde (Befund 3): isFresh() had no upper bound — a heartbeat with a * timestamp in the future (a clock skewed between containers, or a corrupted * value that still happens to parse) would report permanent health for as * long as nobody noticed the clock was wrong. */ it('does not treat a heartbeat from the future as healthy', function () { Settings::set('heartbeat.scheduler', now()->addHour()->toIso8601String()); expect(operationCheck('operation.scheduler')->satisfied)->toBeFalse(); });