CluPilotCloud/tests/Feature/WatchdogVisibilityTest.php

132 lines
4.9 KiB
PHP

<?php
use App\Services\Deployment\WatchdogLog;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Process;
/**
* Der Waechter redet bisher nur ins Journal — und das Journal liegt auf dem
* WIRT, waehrend die Konsole in einem Container laeuft. Sie sieht ihn also
* gar nicht. Am 4. August 2026 hat genau das die Fehlersuche gekostet: der
* Waechter hielt die Sperre, der Agent kam nicht an die Arbeit, und die
* einzige Stelle, an der das gestanden haette, war unerreichbar.
*
* Hier laeuft das ECHTE Skript gegen eine docker-Attrappe.
*/
function runWatchdog(string $running = "app\nredis", bool $holdLock = false): array
{
$dir = storage_path('app/deploy');
File::ensureDirectoryExists($dir);
File::delete(File::glob($dir.'/*'));
$stub = $dir.'/stub';
File::ensureDirectoryExists($stub);
// '$running' bewusst in Anfuehrungszeichen: der Vorgabewert "app\nredis"
// traegt einen echten Zeilenumbruch, und ohne Quoting zerlegt genau der
// die generierte Zeile in zwei Shell-Befehle — `printf … app` und ein
// eigenstaendiges `redis`, das als unbekannter Befehl scheitert. `ps`
// meldete dann nur "app" und der Waechter sah faelschlich "redis" als
// fehlenden Dienst. In Anfuehrungszeichen bleibt der Zeilenumbruch Teil
// des einen printf-Arguments, so wie es die `config`-Zeile ohnehin schon
// (mit zwei getrennten Woertern statt einem Wert) richtig macht.
File::put($stub.'/docker', <<<SH
#!/bin/sh
case "\$*" in
*config*) printf '%s\\n' app redis ;;
*"ps "*) printf '%s\\n' '$running' ;;
*getent*) exit 0 ;;
*storage/framework/down*) exit 1 ;;
*" up "*) touch "\$STUB_UP_CALLED" ;;
esac
exit 0
SH);
Process::run("chmod +x {$stub}/docker");
$hold = $holdLock
? "flock storage/app/deploy/.agent.lock -c 'touch storage/app/deploy/.held; sleep 20' &\n"
."until [ -f storage/app/deploy/.held ]; do sleep 0.05; done\n"
: '';
$result = Process::path(base_path())->timeout(90)->env([
'PATH' => $stub.':'.env('PATH', '/usr/local/bin:/usr/bin:/bin'),
'STUB_UP_CALLED' => $dir.'/.stub-up-called',
])->run(<<<BASH
{$hold}
bash deploy/watchdog.sh >/dev/null 2>&1 || true
pkill -f 'sleep 20' 2>/dev/null || true
BASH);
expect($result->successful())->toBeTrue($result->errorOutput());
return json_decode(File::get($dir.'/watchdog-last-run.json'), true);
}
afterEach(function () {
File::deleteDirectory(storage_path('app/deploy'));
});
it('records a run where there was nothing to do', function () {
$run = runWatchdog();
expect($run['outcome'])->toBe('idle')
->and($run['actions'])->toBe([])
->and($run['at'])->not->toBeEmpty();
});
it('records what it healed', function () {
// `app` fehlt in der Liste der laufenden Dienste — der Waechter startet
// die Dienste und muss das hinterlassen.
$run = runWatchdog(running: 'redis');
expect($run['outcome'])->toBe('healed')
->and($run['actions'])->not->toBeEmpty();
});
it('records that it stood down because the lock was held', function () {
// DER Zustand, der bisher unsichtbar war. Ohne ihn sieht ein Waechter,
// der seit einer Stunde nicht eingreifen kann, genauso aus wie einer,
// der nichts zu tun hat.
$run = runWatchdog(running: 'redis', holdLock: true);
expect($run['outcome'])->toBe('stood_down');
});
it('reads nothing rather than falling over when the file is absent', function () {
File::ensureDirectoryExists(storage_path('app/deploy'));
expect(app(WatchdogLog::class)->lastRun())->toBeNull();
});
it('reads nothing rather than falling over when the file is rubbish', function () {
File::ensureDirectoryExists(storage_path('app/deploy'));
File::put(storage_path('app/deploy/watchdog-last-run.json'), 'kein json {');
expect(app(WatchdogLog::class)->lastRun())->toBeNull();
});
it('calls a run from long ago stale', function () {
// Ein toter Waechter muss als solcher lesbar sein. Bisher wuerde niemand
// es je erfahren.
File::ensureDirectoryExists(storage_path('app/deploy'));
File::put(storage_path('app/deploy/watchdog-last-run.json'), json_encode([
'at' => now()->subMinutes(30)->utc()->format('Y-m-d\TH:i:s\Z'),
'outcome' => 'idle',
'actions' => [],
]));
$run = app(WatchdogLog::class)->lastRun();
expect($run['stale'])->toBeTrue();
});
it('does not call a fresh run stale', function () {
File::ensureDirectoryExists(storage_path('app/deploy'));
File::put(storage_path('app/deploy/watchdog-last-run.json'), json_encode([
'at' => now()->utc()->format('Y-m-d\TH:i:s\Z'),
'outcome' => 'idle',
'actions' => [],
]));
expect(app(WatchdogLog::class)->lastRun()['stale'])->toBeFalse();
});