584 lines
22 KiB
TypeScript
584 lines
22 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 AionUi (aionui.com)
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import path from 'path';
|
|
import { rmSync } from 'fs';
|
|
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
const autoUpdaterMock = vi.hoisted(() => ({
|
|
logger: null as unknown,
|
|
autoDownload: true,
|
|
autoInstallOnAppQuit: false,
|
|
forceDevUpdateConfig: false,
|
|
allowPrerelease: false,
|
|
allowDowngrade: false,
|
|
channel: undefined as string | undefined,
|
|
currentVersion: { version: '2.1.13' },
|
|
setFeedURL: vi.fn(),
|
|
on: vi.fn(),
|
|
removeListener: vi.fn(),
|
|
checkForUpdates: vi.fn(),
|
|
downloadUpdate: vi.fn(),
|
|
getOrCreateDownloadHelper: vi.fn(),
|
|
quitAndInstall: vi.fn(),
|
|
checkForUpdatesAndNotify: vi.fn(),
|
|
}));
|
|
|
|
const nativeAutoUpdaterMock = vi.hoisted(() => ({
|
|
on: vi.fn(),
|
|
removeListener: vi.fn(),
|
|
}));
|
|
|
|
const appMock = vi.hoisted(() => ({
|
|
isPackaged: false,
|
|
getVersion: vi.fn(() => '2.1.13'),
|
|
getPath: vi.fn(() => '/tmp/aionui-test'),
|
|
exit: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('electron-updater', () => ({
|
|
autoUpdater: autoUpdaterMock,
|
|
}));
|
|
|
|
vi.mock('electron', () => ({
|
|
app: appMock,
|
|
autoUpdater: nativeAutoUpdaterMock,
|
|
}));
|
|
|
|
vi.mock('@/process/services/i18n', () => ({
|
|
default: {
|
|
t: (key: string) => key,
|
|
},
|
|
i18nReady: Promise.resolve(),
|
|
}));
|
|
|
|
vi.mock('electron-log', () => ({
|
|
default: {
|
|
transports: { file: { level: 'info' } },
|
|
debug: vi.fn(),
|
|
info: vi.fn(),
|
|
error: vi.fn(),
|
|
warn: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
const setPlatform = (platform: NodeJS.Platform): void => {
|
|
Object.defineProperty(process, 'platform', {
|
|
configurable: true,
|
|
value: platform,
|
|
});
|
|
};
|
|
|
|
describe('AutoUpdaterService', () => {
|
|
const originalPlatform = process.platform;
|
|
|
|
beforeEach(() => {
|
|
vi.resetModules();
|
|
vi.clearAllMocks();
|
|
setPlatform(originalPlatform);
|
|
autoUpdaterMock.logger = null;
|
|
autoUpdaterMock.autoDownload = true;
|
|
autoUpdaterMock.autoInstallOnAppQuit = false;
|
|
autoUpdaterMock.forceDevUpdateConfig = false;
|
|
autoUpdaterMock.allowPrerelease = false;
|
|
autoUpdaterMock.allowDowngrade = false;
|
|
autoUpdaterMock.channel = undefined;
|
|
appMock.getPath.mockImplementation(() => '/tmp/aionui-test');
|
|
delete (autoUpdaterMock as { updateInfoAndProvider?: unknown }).updateInfoAndProvider;
|
|
appMock.isPackaged = false;
|
|
delete process.env.AIONUI_FORCE_DEV_AUTO_UPDATE;
|
|
delete process.env.AIONUI_DEBUG_AUTO_UPDATE_CURRENT_VERSION;
|
|
nativeAutoUpdaterMock.on.mockReset();
|
|
nativeAutoUpdaterMock.removeListener.mockReset();
|
|
Object.defineProperty(autoUpdaterMock, 'currentVersion', {
|
|
configurable: true,
|
|
value: { version: '2.1.13' },
|
|
});
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.clearAllTimers();
|
|
vi.useRealTimers();
|
|
setPlatform(originalPlatform);
|
|
});
|
|
|
|
it('does not use the stable CDN updater when prerelease manual mode is enabled', async () => {
|
|
autoUpdaterMock.checkForUpdates.mockResolvedValue({
|
|
isUpdateAvailable: true,
|
|
updateInfo: {
|
|
version: '2.1.14',
|
|
files: [{ url: 'AionUi-2.1.14-mac-arm64.dmg', sha512: 'sha512-value' }],
|
|
path: 'AionUi-2.1.14-mac-arm64.dmg',
|
|
sha512: 'sha512-value',
|
|
releaseDate: '2026-06-08T00:00:00.000Z',
|
|
},
|
|
});
|
|
|
|
const { autoUpdaterService } = await import('@/process/services/autoUpdaterService');
|
|
|
|
autoUpdaterService.initialize();
|
|
autoUpdaterService.setAllowPrerelease(true);
|
|
|
|
const result = await autoUpdaterService.checkForUpdates();
|
|
|
|
expect(result).toEqual({ success: true });
|
|
expect(autoUpdaterMock.checkForUpdates).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('configures electron-updater to read stable metadata from the CDN', async () => {
|
|
const { autoUpdaterService } = await import('@/process/services/autoUpdaterService');
|
|
const { CdnGenericProvider } = await import('@/process/services/cdnGenericProvider');
|
|
|
|
autoUpdaterService.resetForTest();
|
|
|
|
expect(autoUpdaterMock.setFeedURL).toHaveBeenCalledWith({
|
|
provider: 'custom',
|
|
url: 'https://static.aionui.com/releases',
|
|
updateProvider: CdnGenericProvider,
|
|
});
|
|
});
|
|
|
|
it('enables forced updater checks in unpacked dev builds when requested', async () => {
|
|
process.env.AIONUI_FORCE_DEV_AUTO_UPDATE = '1';
|
|
|
|
await import('@/process/services/autoUpdaterService');
|
|
|
|
expect(autoUpdaterMock.forceDevUpdateConfig).toBe(true);
|
|
});
|
|
|
|
it('overrides the updater current version only for forced unpacked dev checks', async () => {
|
|
process.env.AIONUI_FORCE_DEV_AUTO_UPDATE = '1';
|
|
process.env.AIONUI_DEBUG_AUTO_UPDATE_CURRENT_VERSION = '2.1.12';
|
|
|
|
await import('@/process/services/autoUpdaterService');
|
|
|
|
expect(autoUpdaterMock.currentVersion.version).toBe('2.1.12');
|
|
});
|
|
|
|
it('ignores forced updater debug env in packaged builds', async () => {
|
|
appMock.isPackaged = true;
|
|
process.env.AIONUI_FORCE_DEV_AUTO_UPDATE = '1';
|
|
process.env.AIONUI_DEBUG_AUTO_UPDATE_CURRENT_VERSION = '2.1.12';
|
|
|
|
await import('@/process/services/autoUpdaterService');
|
|
|
|
expect(autoUpdaterMock.forceDevUpdateConfig).toBe(false);
|
|
expect(autoUpdaterMock.currentVersion.version).toBe('2.1.13');
|
|
});
|
|
|
|
const getErrorHandler = (): ((error: Error) => void) => {
|
|
const entry = autoUpdaterMock.on.mock.calls.find(([event]) => event === 'error');
|
|
if (!entry) throw new Error('error handler not registered');
|
|
return entry[1] as (error: Error) => void;
|
|
};
|
|
|
|
const getUpdateDownloadedHandler = (): ((info: { version: string }) => void) => {
|
|
const entry = autoUpdaterMock.on.mock.calls.find(([event]) => event === 'update-downloaded');
|
|
if (!entry) throw new Error('update-downloaded handler not registered');
|
|
return entry[1] as (info: { version: string }) => void;
|
|
};
|
|
|
|
const getNativeUpdateDownloadedHandler = (): (() => void) => {
|
|
const entry = nativeAutoUpdaterMock.on.mock.calls.find(([event]) => event === 'update-downloaded');
|
|
if (!entry) throw new Error('native update-downloaded handler not registered');
|
|
return entry[1] as () => void;
|
|
};
|
|
|
|
const getNativeErrorHandler = (): ((error: Error) => void) => {
|
|
const entry = nativeAutoUpdaterMock.on.mock.calls.find(([event]) => event === 'error');
|
|
if (!entry) throw new Error('native error handler not registered');
|
|
return entry[1] as (error: Error) => void;
|
|
};
|
|
|
|
const getCheckingForUpdateHandler = (): (() => void) => {
|
|
const entry = autoUpdaterMock.on.mock.calls.find(([event]) => event === 'checking-for-update');
|
|
if (!entry) throw new Error('checking-for-update handler not registered');
|
|
return entry[1] as () => void;
|
|
};
|
|
|
|
it('clarifies the Squirrel bundle error in dev mode', async () => {
|
|
appMock.isPackaged = false;
|
|
const { autoUpdaterService } = await import('@/process/services/autoUpdaterService');
|
|
autoUpdaterService.initialize();
|
|
|
|
const statuses: Array<{ status: string; error?: string }> = [];
|
|
autoUpdaterService.on('update-status', (s: { status: string; error?: string }) => statuses.push(s));
|
|
|
|
getErrorHandler()(new Error('Could not locate update bundle for com.github.Electron within file:///tmp/x'));
|
|
|
|
const errorStatus = statuses.find((s) => s.status === 'error');
|
|
expect(errorStatus?.error).toContain('[dev]');
|
|
expect(errorStatus?.error).toContain('Could not locate update bundle');
|
|
});
|
|
|
|
it('passes through unrelated auto-updater errors verbatim', async () => {
|
|
appMock.isPackaged = false;
|
|
const { autoUpdaterService } = await import('@/process/services/autoUpdaterService');
|
|
autoUpdaterService.initialize();
|
|
|
|
const statuses: Array<{ status: string; error?: string }> = [];
|
|
autoUpdaterService.on('update-status', (s: { status: string; error?: string }) => statuses.push(s));
|
|
|
|
getErrorHandler()(new Error('network timeout'));
|
|
|
|
const errorStatus = statuses.find((s) => s.status === 'error');
|
|
expect(errorStatus?.error).toBe('network timeout');
|
|
});
|
|
|
|
it('reuses the active auto-update download promise', async () => {
|
|
let resolveDownload!: () => void;
|
|
autoUpdaterMock.downloadUpdate.mockImplementation(
|
|
() =>
|
|
new Promise<void>((resolve) => {
|
|
resolveDownload = resolve;
|
|
})
|
|
);
|
|
|
|
const { autoUpdaterService } = await import('@/process/services/autoUpdaterService');
|
|
autoUpdaterService.initialize();
|
|
|
|
const first = autoUpdaterService.downloadUpdate();
|
|
const second = autoUpdaterService.downloadUpdate();
|
|
|
|
expect(autoUpdaterMock.downloadUpdate).toHaveBeenCalledTimes(1);
|
|
|
|
resolveDownload();
|
|
await expect(first).resolves.toEqual({ success: true });
|
|
await expect(second).resolves.toEqual({ success: true });
|
|
});
|
|
|
|
it('allows a new auto-update download after a terminal updater event', async () => {
|
|
autoUpdaterMock.downloadUpdate.mockResolvedValue(undefined);
|
|
|
|
const { autoUpdaterService } = await import('@/process/services/autoUpdaterService');
|
|
autoUpdaterService.initialize();
|
|
|
|
await expect(autoUpdaterService.downloadUpdate()).resolves.toEqual({ success: true });
|
|
getUpdateDownloadedHandler()({ version: '2.2.0' });
|
|
await expect(autoUpdaterService.downloadUpdate()).resolves.toEqual({ success: true });
|
|
|
|
expect(autoUpdaterMock.downloadUpdate).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it('passes a cancellation token to auto-updater and cancels it on cancelDownload', async () => {
|
|
let resolveDownload!: () => void;
|
|
autoUpdaterMock.downloadUpdate.mockImplementation(
|
|
() =>
|
|
new Promise<void>((resolve) => {
|
|
resolveDownload = resolve;
|
|
})
|
|
);
|
|
|
|
const { autoUpdaterService } = await import('@/process/services/autoUpdaterService');
|
|
autoUpdaterService.initialize();
|
|
|
|
const download = autoUpdaterService.downloadUpdate();
|
|
|
|
const token = autoUpdaterMock.downloadUpdate.mock.calls[0]?.[0] as
|
|
| { cancel: () => void; cancelled: boolean }
|
|
| undefined;
|
|
expect(token).toBeTruthy();
|
|
expect(token?.cancelled).toBe(false);
|
|
|
|
await expect(autoUpdaterService.cancelDownload()).resolves.toEqual({ success: true });
|
|
|
|
expect(token?.cancelled).toBe(true);
|
|
|
|
resolveDownload();
|
|
await expect(download).resolves.toEqual({ success: true });
|
|
});
|
|
|
|
it('restores a completed cached auto-update when the downloaded package validates', async () => {
|
|
const updateInfo = {
|
|
version: '2.1.14',
|
|
files: [{ url: 'AionUi-2.1.14-mac.zip', sha512: 'sha512-value' }],
|
|
path: 'AionUi-2.1.14-mac.zip',
|
|
sha512: 'sha512-value',
|
|
releaseDate: '2026-06-08T00:00:00.000Z',
|
|
};
|
|
const fileInfo = {
|
|
url: new URL('https://static.aionui.com/releases/2.1.14/AionUi-2.1.14-mac.zip'),
|
|
info: { url: 'AionUi-2.1.14-mac.zip', sha512: 'sha512-value' },
|
|
};
|
|
const cachedUpdatePath = path.join('/cache/pending', 'AionUi-2.1.14-mac.zip');
|
|
const validateDownloadedPath = vi.fn().mockResolvedValue(cachedUpdatePath);
|
|
|
|
autoUpdaterMock.checkForUpdates.mockImplementation(async () => {
|
|
(autoUpdaterMock as { updateInfoAndProvider?: unknown }).updateInfoAndProvider = {
|
|
info: updateInfo,
|
|
provider: { resolveFiles: vi.fn(() => [fileInfo]) },
|
|
};
|
|
return { isUpdateAvailable: true, updateInfo };
|
|
});
|
|
autoUpdaterMock.getOrCreateDownloadHelper.mockResolvedValue({
|
|
cacheDirForPendingUpdate: '/cache/pending',
|
|
validateDownloadedPath,
|
|
});
|
|
autoUpdaterMock.downloadUpdate.mockResolvedValue(undefined);
|
|
|
|
const { autoUpdaterService } = await import('@/process/services/autoUpdaterService');
|
|
autoUpdaterService.initialize();
|
|
|
|
await expect(autoUpdaterService.restoreDownloadedUpdateIfAvailable()).resolves.toEqual({
|
|
success: true,
|
|
data: {
|
|
ready: true,
|
|
version: '2.1.14',
|
|
currentVersion: '2.1.13',
|
|
filePath: cachedUpdatePath,
|
|
},
|
|
});
|
|
expect(validateDownloadedPath).toHaveBeenCalledWith(cachedUpdatePath, updateInfo, fileInfo, expect.anything());
|
|
expect(autoUpdaterMock.downloadUpdate).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('does not restore a cached auto-update when the downloaded package is missing or invalid', async () => {
|
|
const updateInfo = {
|
|
version: '2.1.14',
|
|
files: [{ url: 'AionUi-2.1.14-mac.zip', sha512: 'sha512-value' }],
|
|
path: 'AionUi-2.1.14-mac.zip',
|
|
sha512: 'sha512-value',
|
|
releaseDate: '2026-06-08T00:00:00.000Z',
|
|
};
|
|
const fileInfo = {
|
|
url: new URL('https://static.aionui.com/releases/2.1.14/AionUi-2.1.14-mac.zip'),
|
|
info: { url: 'AionUi-2.1.14-mac.zip', sha512: 'sha512-value' },
|
|
};
|
|
const validateDownloadedPath = vi.fn().mockResolvedValue(null);
|
|
|
|
autoUpdaterMock.checkForUpdates.mockImplementation(async () => {
|
|
(autoUpdaterMock as { updateInfoAndProvider?: unknown }).updateInfoAndProvider = {
|
|
info: updateInfo,
|
|
provider: { resolveFiles: vi.fn(() => [fileInfo]) },
|
|
};
|
|
return { isUpdateAvailable: true, updateInfo };
|
|
});
|
|
autoUpdaterMock.getOrCreateDownloadHelper.mockResolvedValue({
|
|
cacheDirForPendingUpdate: '/cache/pending',
|
|
validateDownloadedPath,
|
|
});
|
|
|
|
const { autoUpdaterService } = await import('@/process/services/autoUpdaterService');
|
|
autoUpdaterService.initialize();
|
|
|
|
await expect(autoUpdaterService.restoreDownloadedUpdateIfAvailable()).resolves.toEqual({
|
|
success: true,
|
|
data: { ready: false },
|
|
});
|
|
expect(autoUpdaterMock.downloadUpdate).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('waits on macOS when service downloaded but native install readiness has not arrived', async () => {
|
|
setPlatform('darwin');
|
|
const cleanup = vi.fn();
|
|
const statuses: Array<{ status: string; error?: string }> = [];
|
|
const { autoUpdaterService } = await import('@/process/services/autoUpdaterService');
|
|
|
|
autoUpdaterService.initialize();
|
|
autoUpdaterService.setBeforeQuitAndInstall(cleanup);
|
|
autoUpdaterService.on('update-status', (status: { status: string; error?: string }) => statuses.push(status));
|
|
getUpdateDownloadedHandler()({ version: '2.2.0' });
|
|
|
|
const installPromise = autoUpdaterService.quitAndInstall();
|
|
await Promise.resolve();
|
|
|
|
expect(statuses.some((status) => status.status === 'preparing-install')).toBe(true);
|
|
expect(cleanup).not.toHaveBeenCalled();
|
|
expect(autoUpdaterMock.quitAndInstall).not.toHaveBeenCalled();
|
|
|
|
getNativeUpdateDownloadedHandler()();
|
|
await installPromise;
|
|
|
|
expect(cleanup).toHaveBeenCalledTimes(1);
|
|
expect(autoUpdaterMock.quitAndInstall).toHaveBeenCalledWith(false, true);
|
|
});
|
|
|
|
it('does not quit on macOS when native updater reports readiness error first', async () => {
|
|
setPlatform('darwin');
|
|
const statuses: Array<{ status: string; error?: string }> = [];
|
|
const { autoUpdaterService } = await import('@/process/services/autoUpdaterService');
|
|
|
|
autoUpdaterService.initialize();
|
|
autoUpdaterService.on('update-status', (status: { status: string; error?: string }) => statuses.push(status));
|
|
getUpdateDownloadedHandler()({ version: '2.2.0' });
|
|
|
|
const installPromise = autoUpdaterService.quitAndInstall();
|
|
await Promise.resolve();
|
|
getNativeErrorHandler()(new Error('native failed'));
|
|
|
|
await expect(installPromise).rejects.toThrow('update.errors.prepareInstallFailed');
|
|
expect(autoUpdaterMock.quitAndInstall).not.toHaveBeenCalled();
|
|
expect(statuses.at(-1)).toEqual({
|
|
status: 'error',
|
|
error: 'update.errors.prepareInstallFailed',
|
|
});
|
|
});
|
|
|
|
it('does not quit on macOS when native updater readiness times out', async () => {
|
|
setPlatform('darwin');
|
|
vi.useFakeTimers();
|
|
const statuses: Array<{ status: string; error?: string }> = [];
|
|
const { autoUpdaterService } = await import('@/process/services/autoUpdaterService');
|
|
|
|
autoUpdaterService.initialize();
|
|
autoUpdaterService.on('update-status', (status: { status: string; error?: string }) => statuses.push(status));
|
|
getUpdateDownloadedHandler()({ version: '2.2.0' });
|
|
|
|
const installPromise = autoUpdaterService.quitAndInstall();
|
|
await Promise.resolve();
|
|
const installRejection = expect(installPromise).rejects.toThrow('update.errors.prepareInstallTimeout');
|
|
await vi.advanceTimersByTimeAsync(60_000);
|
|
|
|
await installRejection;
|
|
expect(autoUpdaterMock.quitAndInstall).not.toHaveBeenCalled();
|
|
expect(statuses.at(-1)).toEqual({
|
|
status: 'error',
|
|
error: 'update.errors.prepareInstallTimeout',
|
|
});
|
|
});
|
|
|
|
it('resets macOS native readiness when a new update check starts', async () => {
|
|
setPlatform('darwin');
|
|
const cleanup = vi.fn();
|
|
const { autoUpdaterService } = await import('@/process/services/autoUpdaterService');
|
|
|
|
autoUpdaterService.initialize();
|
|
autoUpdaterService.setBeforeQuitAndInstall(cleanup);
|
|
getUpdateDownloadedHandler()({ version: '2.2.0' });
|
|
getNativeUpdateDownloadedHandler()();
|
|
getCheckingForUpdateHandler()();
|
|
getUpdateDownloadedHandler()({ version: '2.3.0' });
|
|
|
|
const installPromise = autoUpdaterService.quitAndInstall();
|
|
await Promise.resolve();
|
|
|
|
expect(cleanup).not.toHaveBeenCalled();
|
|
expect(autoUpdaterMock.quitAndInstall).not.toHaveBeenCalled();
|
|
|
|
getNativeUpdateDownloadedHandler()();
|
|
await installPromise;
|
|
|
|
expect(cleanup).toHaveBeenCalledTimes(1);
|
|
expect(autoUpdaterMock.quitAndInstall).toHaveBeenCalledWith(false, true);
|
|
});
|
|
|
|
it('rejects a pending macOS install wait when a new update check starts', async () => {
|
|
setPlatform('darwin');
|
|
const { autoUpdaterService } = await import('@/process/services/autoUpdaterService');
|
|
|
|
autoUpdaterService.initialize();
|
|
getUpdateDownloadedHandler()({ version: '2.2.0' });
|
|
|
|
const installPromise = autoUpdaterService.quitAndInstall();
|
|
await Promise.resolve();
|
|
const installRejection = expect(installPromise).rejects.toThrow('update.errors.prepareInstallFailed');
|
|
|
|
getCheckingForUpdateHandler()();
|
|
|
|
await installRejection;
|
|
expect(autoUpdaterMock.quitAndInstall).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('broadcasts an error when pre-install cleanup fails after macOS readiness', async () => {
|
|
setPlatform('darwin');
|
|
const cleanupError = new Error('cleanup failed');
|
|
const statuses: Array<{ status: string; error?: string }> = [];
|
|
const { autoUpdaterService } = await import('@/process/services/autoUpdaterService');
|
|
|
|
autoUpdaterService.initialize();
|
|
autoUpdaterService.setBeforeQuitAndInstall(async () => {
|
|
throw cleanupError;
|
|
});
|
|
autoUpdaterService.on('update-status', (status: { status: string; error?: string }) => statuses.push(status));
|
|
getUpdateDownloadedHandler()({ version: '2.2.0' });
|
|
|
|
const installPromise = autoUpdaterService.quitAndInstall();
|
|
await Promise.resolve();
|
|
getNativeUpdateDownloadedHandler()();
|
|
|
|
await expect(installPromise).rejects.toThrow('cleanup failed');
|
|
expect(autoUpdaterMock.quitAndInstall).not.toHaveBeenCalled();
|
|
expect(statuses.at(-1)).toEqual({
|
|
status: 'error',
|
|
error: 'update.errors.prepareInstallFailed',
|
|
});
|
|
});
|
|
|
|
it('does not force exit when quitAndInstall handoff throws', async () => {
|
|
setPlatform('darwin');
|
|
const statuses: Array<{ status: string; error?: string }> = [];
|
|
const { autoUpdaterService } = await import('@/process/services/autoUpdaterService');
|
|
|
|
autoUpdaterMock.quitAndInstall.mockImplementationOnce(() => {
|
|
throw new Error('handoff failed');
|
|
});
|
|
autoUpdaterService.initialize();
|
|
autoUpdaterService.on('update-status', (status: { status: string; error?: string }) => statuses.push(status));
|
|
getUpdateDownloadedHandler()({ version: '2.2.0' });
|
|
getNativeUpdateDownloadedHandler()();
|
|
|
|
await expect(autoUpdaterService.quitAndInstall()).rejects.toThrow('update.errors.prepareInstallFailed');
|
|
|
|
expect(appMock.exit).not.toHaveBeenCalled();
|
|
expect(statuses.at(-1)).toEqual({
|
|
status: 'error',
|
|
error: 'update.errors.prepareInstallFailed',
|
|
});
|
|
});
|
|
|
|
it('keeps non-macOS quitAndInstall behavior immediate', async () => {
|
|
setPlatform('win32');
|
|
const cleanup = vi.fn();
|
|
const { autoUpdaterService } = await import('@/process/services/autoUpdaterService');
|
|
|
|
autoUpdaterService.initialize();
|
|
autoUpdaterService.setBeforeQuitAndInstall(cleanup);
|
|
|
|
await autoUpdaterService.quitAndInstall();
|
|
|
|
expect(cleanup).toHaveBeenCalledTimes(1);
|
|
expect(autoUpdaterMock.quitAndInstall).toHaveBeenCalledWith(false, true);
|
|
expect(nativeAutoUpdaterMock.on).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('uses a non-silent handoff for user-initiated Windows installs without changing app-quit installs', async () => {
|
|
setPlatform('win32');
|
|
const cleanup = vi.fn();
|
|
const { autoUpdaterService } = await import('@/process/services/autoUpdaterService');
|
|
|
|
autoUpdaterService.initialize();
|
|
autoUpdaterService.setBeforeQuitAndInstall(cleanup);
|
|
|
|
await autoUpdaterService.quitAndInstall();
|
|
|
|
expect(cleanup).toHaveBeenCalledTimes(1);
|
|
expect(autoUpdaterMock.autoInstallOnAppQuit).toBe(true);
|
|
expect(autoUpdaterMock.quitAndInstall).toHaveBeenCalledWith(false, true);
|
|
});
|
|
|
|
it('moves the process cwd to temp before the Windows updater handoff', async () => {
|
|
setPlatform('win32');
|
|
const tempRoot = path.join(process.env.TEMP || process.cwd(), `aionui-updater-cwd-test-${process.pid}`);
|
|
const expectedCwd = path.join(tempRoot, 'aionui-updater-cwd');
|
|
const chdir = vi.spyOn(process, 'chdir').mockImplementation(() => undefined);
|
|
appMock.getPath.mockImplementation((name: string) => (name === 'temp' ? tempRoot : '/tmp/aionui-test'));
|
|
|
|
try {
|
|
const { autoUpdaterService } = await import('@/process/services/autoUpdaterService');
|
|
autoUpdaterService.initialize();
|
|
|
|
await autoUpdaterService.quitAndInstall();
|
|
|
|
expect(chdir).toHaveBeenCalledWith(expectedCwd);
|
|
expect(autoUpdaterMock.quitAndInstall).toHaveBeenCalledWith(false, true);
|
|
} finally {
|
|
chdir.mockRestore();
|
|
rmSync(tempRoot, { recursive: true, force: true });
|
|
}
|
|
});
|
|
});
|