97 lines
3.7 KiB
PHP
97 lines
3.7 KiB
PHP
<?php
|
|
|
|
use App\Provisioning\Jobs\RecordProvisioningHeartbeat;
|
|
use App\Support\Readiness;
|
|
use App\Support\Readiness\Check;
|
|
use App\Support\Settings;
|
|
use Illuminate\Support\Facades\Queue;
|
|
|
|
function operationCheck(string $key): ?Check
|
|
{
|
|
return collect(Readiness::all())->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();
|
|
});
|