mirror of
https://github.com/davidkaya/aryx.git
synced 2026-08-07 04:08:45 +02:00
feat: migrate packaging and auto updates
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
import { EventEmitter } from 'node:events';
|
||||
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
|
||||
import { AutoUpdateService, type AutoUpdateScheduler } from '@main/services/autoUpdater';
|
||||
import type { UpdateStatus } from '@shared/contracts/ipc';
|
||||
|
||||
class FakeUpdater extends EventEmitter {
|
||||
autoDownload = false;
|
||||
|
||||
autoInstallOnAppQuit = true;
|
||||
|
||||
checkForUpdatesCalls = 0;
|
||||
|
||||
quitAndInstallCalls = 0;
|
||||
|
||||
async checkForUpdates(): Promise<void> {
|
||||
this.checkForUpdatesCalls += 1;
|
||||
}
|
||||
|
||||
quitAndInstall(): void {
|
||||
this.quitAndInstallCalls += 1;
|
||||
}
|
||||
}
|
||||
|
||||
class FakeScheduler implements AutoUpdateScheduler {
|
||||
readonly timeouts: Array<{ callback: () => void; delayMs: number }> = [];
|
||||
|
||||
readonly intervals: Array<{ callback: () => void; delayMs: number }> = [];
|
||||
|
||||
setTimeout(callback: () => void, delayMs: number): number {
|
||||
this.timeouts.push({ callback, delayMs });
|
||||
return this.timeouts.length - 1;
|
||||
}
|
||||
|
||||
clearTimeout(handle: unknown): void {
|
||||
const index = Number(handle);
|
||||
if (Number.isInteger(index) && index >= 0) {
|
||||
this.timeouts.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
setInterval(callback: () => void, delayMs: number): number {
|
||||
this.intervals.push({ callback, delayMs });
|
||||
return this.intervals.length - 1;
|
||||
}
|
||||
|
||||
clearInterval(handle: unknown): void {
|
||||
const index = Number(handle);
|
||||
if (Number.isInteger(index) && index >= 0) {
|
||||
this.intervals.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
async runTimeout(index = 0): Promise<void> {
|
||||
await Promise.resolve(this.timeouts[index]?.callback());
|
||||
}
|
||||
|
||||
async runInterval(index = 0): Promise<void> {
|
||||
await Promise.resolve(this.intervals[index]?.callback());
|
||||
}
|
||||
}
|
||||
|
||||
describe('AutoUpdateService', () => {
|
||||
test('does not schedule checks for unpackaged apps', async () => {
|
||||
const updater = new FakeUpdater();
|
||||
const scheduler = new FakeScheduler();
|
||||
const service = new AutoUpdateService({ isPackaged: false, scheduler, updater });
|
||||
|
||||
service.start();
|
||||
|
||||
expect(scheduler.timeouts).toHaveLength(0);
|
||||
expect(scheduler.intervals).toHaveLength(0);
|
||||
expect(await service.checkForUpdates()).toEqual({ state: 'idle' });
|
||||
expect(updater.checkForUpdatesCalls).toBe(0);
|
||||
});
|
||||
|
||||
test('configures auto download and schedules startup and periodic checks', async () => {
|
||||
const updater = new FakeUpdater();
|
||||
const scheduler = new FakeScheduler();
|
||||
const service = new AutoUpdateService({ isPackaged: true, scheduler, updater });
|
||||
|
||||
service.start();
|
||||
|
||||
expect(updater.autoDownload).toBe(true);
|
||||
expect(updater.autoInstallOnAppQuit).toBe(false);
|
||||
expect(scheduler.timeouts).toEqual([{ callback: expect.any(Function), delayMs: 10_000 }]);
|
||||
expect(scheduler.intervals).toEqual([{ callback: expect.any(Function), delayMs: 4 * 60 * 60 * 1000 }]);
|
||||
|
||||
await scheduler.runTimeout();
|
||||
await Promise.resolve();
|
||||
await scheduler.runInterval();
|
||||
await Promise.resolve();
|
||||
|
||||
expect(updater.checkForUpdatesCalls).toBe(2);
|
||||
});
|
||||
|
||||
test('maps updater events into renderer-facing status snapshots', () => {
|
||||
const updater = new FakeUpdater();
|
||||
const service = new AutoUpdateService({ isPackaged: true, updater });
|
||||
const statuses: UpdateStatus[] = [];
|
||||
service.onStatus((status) => statuses.push(status));
|
||||
|
||||
updater.emit('checking-for-update');
|
||||
updater.emit('update-available', {
|
||||
version: '1.2.0',
|
||||
releaseDate: '2026-03-29T00:00:00.000Z',
|
||||
releaseNotes: 'Important fixes.',
|
||||
});
|
||||
updater.emit('download-progress', {
|
||||
bytesPerSecond: 512,
|
||||
percent: 25,
|
||||
total: 400,
|
||||
transferred: 100,
|
||||
});
|
||||
updater.emit('update-downloaded', {
|
||||
version: '1.2.0',
|
||||
releaseDate: '2026-03-29T00:00:00.000Z',
|
||||
releaseNotes: 'Important fixes.',
|
||||
});
|
||||
|
||||
expect(statuses).toEqual([
|
||||
{ state: 'checking' },
|
||||
{
|
||||
state: 'available',
|
||||
version: '1.2.0',
|
||||
releaseDate: '2026-03-29T00:00:00.000Z',
|
||||
releaseNotes: 'Important fixes.',
|
||||
},
|
||||
{
|
||||
state: 'downloading',
|
||||
version: '1.2.0',
|
||||
releaseDate: '2026-03-29T00:00:00.000Z',
|
||||
releaseNotes: 'Important fixes.',
|
||||
downloadProgress: {
|
||||
bytesPerSecond: 512,
|
||||
percent: 25,
|
||||
total: 400,
|
||||
transferred: 100,
|
||||
},
|
||||
},
|
||||
{
|
||||
state: 'downloaded',
|
||||
version: '1.2.0',
|
||||
releaseDate: '2026-03-29T00:00:00.000Z',
|
||||
releaseNotes: 'Important fixes.',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('reports updater errors and only installs once an update is downloaded', () => {
|
||||
const updater = new FakeUpdater();
|
||||
const service = new AutoUpdateService({ isPackaged: true, updater });
|
||||
|
||||
service.installUpdate();
|
||||
expect(updater.quitAndInstallCalls).toBe(0);
|
||||
|
||||
updater.emit('error', new Error('network down'));
|
||||
expect(service.getStatus()).toEqual({
|
||||
state: 'error',
|
||||
error: 'network down',
|
||||
});
|
||||
|
||||
updater.emit('update-downloaded', {
|
||||
version: '1.2.1',
|
||||
releaseDate: '2026-03-29T00:00:00.000Z',
|
||||
releaseNotes: 'Patch release.',
|
||||
});
|
||||
service.installUpdate();
|
||||
|
||||
expect(updater.quitAndInstallCalls).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -1,68 +0,0 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
|
||||
import {
|
||||
macBundleIdentifier,
|
||||
productName,
|
||||
resolveReleaseTarget,
|
||||
} from '../../scripts/releaseTarget';
|
||||
|
||||
describe('resolveReleaseTarget', () => {
|
||||
test('maps Windows x64 to the expected runtime and output names', () => {
|
||||
expect(resolveReleaseTarget('win32', 'x64')).toEqual({
|
||||
platform: 'win32',
|
||||
arch: 'x64',
|
||||
platformLabel: 'windows',
|
||||
dotnetRuntime: 'win-x64',
|
||||
outputDirectoryName: 'Aryx-windows-x64',
|
||||
archiveBaseName: 'Aryx-windows-x64',
|
||||
installerAssetName: 'Aryx-windows-x64-setup.exe',
|
||||
sidecarExecutableName: 'Aryx.AgentHost.exe',
|
||||
packagedExecutableName: 'Aryx.exe',
|
||||
});
|
||||
});
|
||||
|
||||
test('maps macOS arm64 to the expected bundle metadata', () => {
|
||||
expect(resolveReleaseTarget('darwin', 'arm64')).toEqual({
|
||||
platform: 'darwin',
|
||||
arch: 'arm64',
|
||||
platformLabel: 'macos',
|
||||
dotnetRuntime: 'osx-arm64',
|
||||
outputDirectoryName: 'Aryx-macos-arm64',
|
||||
archiveBaseName: 'Aryx-macos-arm64',
|
||||
installerAssetName: 'Aryx-macos-arm64.dmg',
|
||||
sidecarExecutableName: 'Aryx.AgentHost',
|
||||
appBundleName: 'Aryx.app',
|
||||
});
|
||||
});
|
||||
|
||||
test('maps Linux x64 to the expected runtime and output names', () => {
|
||||
expect(resolveReleaseTarget('linux', 'x64')).toEqual({
|
||||
platform: 'linux',
|
||||
arch: 'x64',
|
||||
platformLabel: 'linux',
|
||||
dotnetRuntime: 'linux-x64',
|
||||
outputDirectoryName: 'Aryx-linux-x64',
|
||||
archiveBaseName: 'Aryx-linux-x64',
|
||||
installerAssetName: 'aryx-linux-x64.deb',
|
||||
sidecarExecutableName: 'Aryx.AgentHost',
|
||||
packagedExecutableName: 'Aryx',
|
||||
});
|
||||
});
|
||||
|
||||
test('exports the expected product metadata constants', () => {
|
||||
expect(productName).toBe('Aryx');
|
||||
expect(macBundleIdentifier).toBe('com.davidkaya.aryx');
|
||||
});
|
||||
|
||||
test('rejects unsupported platforms', () => {
|
||||
expect(() => resolveReleaseTarget('freebsd', 'x64')).toThrow(
|
||||
'Unsupported release platform: freebsd',
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects unsupported architectures', () => {
|
||||
expect(() => resolveReleaseTarget('linux', 'ia32')).toThrow(
|
||||
'Unsupported architecture for linux: ia32',
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user