mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-07-26 11:49:16 +00:00
fix: schedule the first image-update check on the cron cadence after a restart (#1491)
In cron mode the service armed the first check for two minutes after boot regardless of the configured schedule, so a restart triggered an out-of-cadence check (a weekly cron would run on every boot, then follow cron). Arm the first check at the next cron fire time instead; interval mode keeps its 2-minute post-boot delay.
This commit is contained in:
@@ -1156,6 +1156,12 @@ describe('ImageUpdateService cron scheduling', () => {
|
||||
mockGetGlobalSettings.mockReturnValue({ developer_mode: '0' });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Tests below switch to fake timers and a pinned clock; reset so the state
|
||||
// does not leak into later tests in this block (or the file).
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('getStatus returns mode and cronExpression fields', () => {
|
||||
const service = ImageUpdateService.getInstance();
|
||||
// Before start/configureFromSettings, defaults apply.
|
||||
@@ -1238,6 +1244,88 @@ describe('ImageUpdateService cron scheduling', () => {
|
||||
expect(delay).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('start() in cron mode arms at the next cron fire, not the 2-minute startup delay', () => {
|
||||
vi.useFakeTimers();
|
||||
// Pin "now" so the next cron fire is deterministic and far beyond the
|
||||
// startup delay (next Monday 03:00 from this Thursday is days away).
|
||||
vi.setSystemTime(new Date('2026-01-01T00:00:00Z'));
|
||||
mockGetGlobalSettings.mockReturnValue({
|
||||
developer_mode: '0',
|
||||
image_update_check_mode: 'cron',
|
||||
image_update_check_cron: '0 3 * * 1',
|
||||
image_update_check_interval_minutes: '120',
|
||||
});
|
||||
const service = ImageUpdateService.getInstance();
|
||||
const checkSpy = vi.spyOn(service as any, 'check').mockResolvedValue(undefined);
|
||||
|
||||
service.start();
|
||||
|
||||
// The first check is scheduled at the cron fire time, not 2 minutes out, so
|
||||
// a restart never triggers an out-of-cadence check.
|
||||
const startupDelay = (ImageUpdateService as any).STARTUP_DELAY_MS as number;
|
||||
const nextAt = service.getStatus().nextCheckAt!;
|
||||
expect(nextAt - Date.now()).toBeGreaterThan(startupDelay);
|
||||
|
||||
// Advancing well past the old startup delay (but before the cron fire) must
|
||||
// not fire a check.
|
||||
vi.advanceTimersByTime(10 * 60 * 1000);
|
||||
expect(checkSpy).not.toHaveBeenCalled();
|
||||
|
||||
// The check fires exactly when the cron schedule says, not merely "later":
|
||||
// advancing to the computed fire time triggers it once.
|
||||
vi.advanceTimersByTime(nextAt - Date.now() + 1000);
|
||||
expect(checkSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
service.stop();
|
||||
checkSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('start() in cron mode fires sooner than the startup delay when the next fire is near', () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date('2026-01-01T00:00:00Z'));
|
||||
mockGetGlobalSettings.mockReturnValue({
|
||||
developer_mode: '0',
|
||||
image_update_check_mode: 'cron',
|
||||
image_update_check_cron: '* * * * *', // every minute
|
||||
image_update_check_interval_minutes: '120',
|
||||
});
|
||||
const service = ImageUpdateService.getInstance();
|
||||
const checkSpy = vi.spyOn(service as any, 'check').mockResolvedValue(undefined);
|
||||
|
||||
service.start();
|
||||
|
||||
// A frequent cron fires before the old 2-minute startup delay would have:
|
||||
// cron mode honors the schedule in both directions, not just "no sooner".
|
||||
const startupDelay = (ImageUpdateService as any).STARTUP_DELAY_MS as number;
|
||||
const nextAt = service.getStatus().nextCheckAt!;
|
||||
expect(nextAt - Date.now()).toBeLessThan(startupDelay);
|
||||
|
||||
vi.advanceTimersByTime(nextAt - Date.now() + 100);
|
||||
expect(checkSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
service.stop();
|
||||
checkSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('start() in interval mode keeps the 2-minute startup delay', () => {
|
||||
vi.useFakeTimers();
|
||||
mockGetGlobalSettings.mockReturnValue({ developer_mode: '0' });
|
||||
const service = ImageUpdateService.getInstance();
|
||||
const checkSpy = vi.spyOn(service as any, 'check').mockResolvedValue(undefined);
|
||||
|
||||
service.start();
|
||||
|
||||
// Just before the 2-minute delay: no check yet.
|
||||
vi.advanceTimersByTime(2 * 60 * 1000 - 1000);
|
||||
expect(checkSpy).not.toHaveBeenCalled();
|
||||
// Crossing the 2-minute mark fires the first check.
|
||||
vi.advanceTimersByTime(1000);
|
||||
expect(checkSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
service.stop();
|
||||
checkSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('nextDelayMs falls back to interval on runtime parse failure', () => {
|
||||
// Set up cron mode, then corrupt the expression at runtime before nextDelayMs.
|
||||
mockGetGlobalSettings.mockReturnValue({
|
||||
|
||||
@@ -220,8 +220,11 @@ export class ImageUpdateService {
|
||||
if (this.timer) return;
|
||||
this.polling = true;
|
||||
this.configureFromSettings();
|
||||
// Preserve the existing 2-minute post-boot delay before the first check.
|
||||
this.armNext(ImageUpdateService.STARTUP_DELAY_MS);
|
||||
// Interval mode keeps the 2-minute post-boot delay before the first check.
|
||||
// Cron mode honors its schedule: arm at the next cron fire time so a restart
|
||||
// never triggers an out-of-cadence check (e.g. a weekly cron must not run on
|
||||
// every boot).
|
||||
this.armNext(this.mode === 'cron' ? this.nextDelayMs() : ImageUpdateService.STARTUP_DELAY_MS);
|
||||
}
|
||||
|
||||
public stop() {
|
||||
|
||||
Reference in New Issue
Block a user