'idle', 'checked_at' => now()->toIso8601String(), 'behind' => 1, ])); } function openWindow(array $days, string $from = '05:00', string $to = '06:00'): void { AppSettings::set(UpdateWindow::KEY_ENABLED, true); AppSettings::set(UpdateWindow::KEY_DAYS, $days); AppSettings::set(UpdateWindow::KEY_FROM, $from); AppSettings::set(UpdateWindow::KEY_TO, $to); } it('does nothing at all while the automation is off', function () { // The switch has to mean it. An installation whose owner deliberately // updates by hand must never find itself in maintenance mode at dawn. windowStatus(); AppSettings::set(UpdateWindow::KEY_ENABLED, false); $this->artisan('clupilot:auto-update')->assertSuccessful(); expect(File::exists(storage_path('app/deploy/update-request.json')))->toBeFalse(); }); it('takes an available release inside the window', function () { // Wednesday, 05:30 local. Carbon::setTestNow(Carbon::parse('2026-07-29 03:30:00', 'UTC')); windowStatus(); openWindow([3]); expect(app(UpdateWindow::class)->isOpen())->toBeTrue(); $this->artisan('clupilot:auto-update')->assertSuccessful(); expect(File::exists(storage_path('app/deploy/update-request.json')))->toBeTrue(); }); it('leaves it alone outside the hours', function () { // Wednesday, 11:30 local — the right day, the wrong time. Carbon::setTestNow(Carbon::parse('2026-07-29 09:30:00', 'UTC')); windowStatus(); openWindow([3]); expect(app(UpdateWindow::class)->isOpen())->toBeFalse(); $this->artisan('clupilot:auto-update'); expect(File::exists(storage_path('app/deploy/update-request.json')))->toBeFalse(); }); it('leaves it alone on a day that was not chosen', function () { Carbon::setTestNow(Carbon::parse('2026-07-29 03:30:00', 'UTC')); // Wednesday windowStatus(); openWindow([1, 2]); // Mon, Tue expect(app(UpdateWindow::class)->isOpen())->toBeFalse(); }); it('understands a window that runs past midnight', function () { // 23:00–02:00 is a perfectly reasonable thing to ask for, and reading it // as an empty range would silently mean "never". $window = app(UpdateWindow::class); // Wednesday 23:30 local — inside, on the chosen day. openWindow([3], '23:00', '02:00'); Carbon::setTestNow(Carbon::parse('2026-07-29 21:30:00', 'UTC')); expect($window->isOpen())->toBeTrue(); // Thursday 01:30 local — still the tail of Wednesday's window. Carbon::setTestNow(Carbon::parse('2026-07-29 23:30:00', 'UTC')); expect($window->isOpen())->toBeTrue(); // Thursday 03:30 local — over. Carbon::setTestNow(Carbon::parse('2026-07-30 01:30:00', 'UTC')); expect($window->isOpen())->toBeFalse(); }); it('finds nothing to do when the operator already updated by hand', function () { // Named by the owner as the case that must not surprise them: pressed at // four, window at five. Nothing guards against it explicitly — an // installation that is current simply has nothing available. Carbon::setTestNow(Carbon::parse('2026-07-29 03:30:00', 'UTC')); File::ensureDirectoryExists(storage_path('app/deploy')); File::put(storage_path('app/deploy/update-status.json'), json_encode([ 'state' => 'idle', 'checked_at' => now()->toIso8601String(), 'behind' => 0, ])); openWindow([3]); expect(app(UpdateChannel::class)->state()['available'])->toBeFalse(); $this->artisan('clupilot:auto-update'); expect(File::exists(storage_path('app/deploy/update-request.json')))->toBeFalse(); }); it('does not queue a second run over one already in flight', function () { Carbon::setTestNow(Carbon::parse('2026-07-29 03:30:00', 'UTC')); windowStatus(); openWindow([3]); app(UpdateChannel::class)->request('owner@example.com'); $before = File::get(storage_path('app/deploy/update-request.json')); $this->artisan('clupilot:auto-update'); expect(File::get(storage_path('app/deploy/update-request.json')))->toBe($before); }); it('will not save an automation that could never run', function () { // Switched on with no day selected is a configuration that reads as working // and does nothing. Refused rather than silently corrected. Livewire::actingAs(operator('Owner'), 'operator') ->test(AdminSettings::class) ->set('autoUpdate', true) ->set('autoDays', []) ->set('autoFrom', '05:00') ->set('autoTo', '06:00') ->call('saveUpdateWindow') ->assertHasErrors('autoDays'); expect(app(UpdateWindow::class)->enabled())->toBeFalse(); }); it('saves a window the owner can see again afterwards', function () { Livewire::actingAs(operator('Owner'), 'operator') ->test(AdminSettings::class) ->set('autoUpdate', true) ->set('autoDays', [2, 4]) ->set('autoFrom', '04:30') ->set('autoTo', '05:30') ->call('saveUpdateWindow') ->assertHasNoErrors(); $window = app(UpdateWindow::class); expect($window->enabled())->toBeTrue() ->and($window->days())->toBe([2, 4]) ->and($window->from())->toBe('04:30') ->and($window->to())->toBe('05:30'); }); it('needs the capability to change when the installation updates itself', function () { Livewire::actingAs(operator('Support'), 'operator') ->test(AdminSettings::class) ->call('saveUpdateWindow') ->assertForbidden(); });