{{ __('admin_settings.update_only_releases') }}
@endif {{-- Where it is. "Läuft gerade" alone is what sends an operator to diff --git a/tests/Feature/Admin/UpdateButtonTest.php b/tests/Feature/Admin/UpdateButtonTest.php index 7194a3c..f63a433 100644 --- a/tests/Feature/Admin/UpdateButtonTest.php +++ b/tests/Feature/Admin/UpdateButtonTest.php @@ -527,13 +527,17 @@ it('keeps the panel, the agent and the installer in step about the on-demand tri ->and($agent)->toContain('"request_watch"'); }); -it('offers "Jetzt aktualisieren" even when the last check found nothing', function () { - // Reported as "I cannot run an update, it says everything is current". - // `behind` is a READING taken up to a minute ago, not the state of the - // world: push a commit and the console insisted it was up to date and - // refused to act on it. The agent fetches before deciding, so asking - // against a stale reading costs one fetch — being locked out costs the - // deployment. +/** + * An update is a released version, never the head of a branch. + * + * The console used to offer "Jetzt aktualisieren" unconditionally, because + * `behind` counted commits on main and was a reading a minute old — refusing on + * a stale reading cost the deployment. That whole problem belonged to following + * a branch. Nothing is installed now unless a tag exists whose version is + * higher than the one deployed, and pressing the check button answers within a + * second, so a stale reading is no longer something to design around. + */ +it('refuses to queue an update when nothing newer has been released', function () { writeStatus([ 'checked_at' => now()->toIso8601String(), 'state' => 'idle', @@ -542,12 +546,65 @@ it('offers "Jetzt aktualisieren" even when the last check found nothing', functi Livewire::actingAs(operator('Owner'), 'operator') ->test(AdminSettings::class) - ->assertSee(__('admin_settings.update_now')) - ->call('requestUpdate'); + ->assertSee(__('admin_settings.update_only_releases')) + ->call('requestUpdate') + ->assertDispatched('notify', message: __('admin_settings.update_nothing_released')); + + // Not merely a disabled button: a Livewire action is a public endpoint, and + // a queued run for nothing costs a maintenance window. + expect(app(UpdateChannel::class)->pendingRequest())->toBeNull(); +}); + +it('offers the update, by version, once a release above the installed one exists', function () { + writeStatus([ + 'checked_at' => now()->toIso8601String(), + 'state' => 'idle', + 'behind' => 1, + 'target_release' => 'v1.1.0', + ]); + + Livewire::actingAs(operator('Owner'), 'operator') + ->test(AdminSettings::class) + // The number somebody can decide about, not "1 Aktualisierung". + ->assertSee(__('admin_settings.update_available_release', ['version' => '1.1.0'])) + ->assertDontSee(__('admin_settings.update_only_releases')) + ->call('requestUpdate') + ->assertDispatched('notify', message: __('admin_settings.update_requested')); expect(app(UpdateChannel::class)->pendingRequest())->not->toBeNull(); }); +it('does not name a release the agent never reported', function () { + // An older agent writes no target_release. The count is all there is, and + // inventing a version number from it would be worse than the count. + writeStatus([ + 'checked_at' => now()->toIso8601String(), + 'state' => 'idle', + 'behind' => 2, + ]); + + expect(app(UpdateChannel::class)->state()['target_release'])->toBeNull(); + + Livewire::actingAs(operator('Owner'), 'operator') + ->test(AdminSettings::class) + ->assertSee(__('admin_settings.update_available', ['n' => 2])); +}); + +it('decides what is newer by version and installs only a tag', function () { + // The agent's half of the same rule, which no PHP test can reach: it asks + // one question regardless of what the checkout is attached to, compares by + // version rather than by branch position, and hands update.sh a RELEASE + // rather than a BRANCH. + $agent = File::get(base_path('deploy/update-agent.sh')); + + expect($agent)->toContain('version_gt') + ->and($agent)->toContain('sort -V') + ->and($agent)->toContain('RELEASE="$TARGET_RELEASE"') + // No branch left to fall back onto, which is what made a commit on main + // installable in the first place. + ->and($agent)->not->toContain('BRANCH="$BRANCH" "$ROOT/deploy/update.sh"'); +}); + it('offers both actions together whenever the agent is reachable and idle', function () { // Two separate buttons now, not one whose label changed with the // reading: looking and applying are different intentions.