fix(updater): handle unsupported macOS app updates

This commit is contained in:
2026-05-16 02:05:28 -07:00
parent d05e2bd8ec
commit 89723e2ccb
8 changed files with 112 additions and 18 deletions
+28 -1
View File
@@ -1,6 +1,10 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { createUpdateDialogPresenter, type ShowMessageBox } from './update-dialogs';
import {
createUpdateDialogPresenter,
showManualUpdateRequiredDialog,
type ShowMessageBox,
} from './update-dialogs';
test('update dialog presenter focuses app before showing macOS dialogs', async () => {
const calls: string[] = [];
@@ -35,3 +39,26 @@ test('update dialog presenter does not focus app before showing non-macOS dialog
assert.deepEqual(calls, ['dialog:SubMiner is up to date (v0.14.0)']);
});
test('manual update required dialog explains that automatic install is unavailable', async () => {
let shown:
| {
type?: string;
title?: string;
message: string;
detail?: string;
buttons?: string[];
}
| undefined;
const showMessageBox: ShowMessageBox = async (options) => {
shown = options;
return { response: 0 };
};
await showManualUpdateRequiredDialog(showMessageBox, '0.15.0-beta.1');
assert.equal(shown?.type, 'warning');
assert.equal(shown?.message, 'Manual install required');
assert.match(shown?.detail ?? '', /SubMiner v0\.15\.0-beta\.1 is available/);
assert.match(shown?.detail ?? '', /cannot install app updates automatically/);
});
+15
View File
@@ -50,6 +50,8 @@ export function createUpdateDialogPresenter(deps: UpdateDialogPresenterDeps) {
showUpdateAvailableDialog(showFocusedMessageBox, version),
showUpdateFailedDialog: (message: string) =>
showUpdateFailedDialog(showFocusedMessageBox, message),
showManualUpdateRequiredDialog: (version: string) =>
showManualUpdateRequiredDialog(showFocusedMessageBox, version),
showRestartDialog: () => showRestartDialog(showFocusedMessageBox),
};
}
@@ -81,6 +83,19 @@ export async function showRestartDialog(showMessageBox: ShowMessageBox): Promise
return result.response === 0 ? 'restart' : 'later';
}
export async function showManualUpdateRequiredDialog(
showMessageBox: ShowMessageBox,
version: string,
): Promise<void> {
await showMessageBox({
type: 'warning',
title: 'SubMiner Updates',
message: 'Manual install required',
detail: `SubMiner v${version} is available, but this build cannot install app updates automatically. Download and install the latest release, then reopen SubMiner.`,
buttons: ['Close'],
});
}
export async function showUpdateFailedDialog(
showMessageBox: ShowMessageBox,
message: string,
+41 -1
View File
@@ -37,6 +37,9 @@ function createDeps(overrides: Partial<UpdateServiceDeps> = {}) {
showUpdateFailedDialog: async (message) => {
calls.push(`failed:${message}`);
},
showManualUpdateRequiredDialog: async (version) => {
calls.push(`manual-install:${version}`);
},
downloadAppUpdate: async () => {
calls.push('download');
},
@@ -115,7 +118,44 @@ test('manual update check reports available when no update asset was applied', a
const result = await service.checkForUpdates({ source: 'manual' });
assert.equal(result.status, 'update-available');
assert.deepEqual(calls, ['available-dialog:0.15.0', 'launcher:stable']);
assert.deepEqual(calls, ['available-dialog:0.15.0', 'launcher:stable', 'manual-install:0.15.0']);
});
test('manual update check does not prompt restart when only launcher updates', async () => {
const { deps, calls } = createDeps({
checkAppUpdate: async () => ({ available: false, version: '0.14.0', canUpdate: false }),
fetchLatestStableRelease: async () => ({
tag_name: 'v0.15.0',
prerelease: false,
draft: false,
assets: [],
}),
showUpdateAvailableDialog: async (version) => {
calls.push(`available-dialog:${version}`);
return 'update';
},
updateLauncher: async (_launcherPath, channel) => {
calls.push(`launcher:${channel}`);
return { status: 'updated' };
},
showRestartDialog: async () => {
calls.push('restart-dialog');
return 'restart';
},
quitAndInstall: () => {
calls.push('quit-install');
},
});
const service = createUpdateService(deps);
const result = await service.checkForUpdates({ source: 'manual' });
assert.equal(result.status, 'update-available');
assert.deepEqual(calls, [
'available-dialog:0.15.0',
'launcher:stable',
'manual-install:0.15.0',
]);
});
test('automatic update check skips inside configured interval', async () => {
+5 -3
View File
@@ -48,6 +48,7 @@ export interface UpdateServiceDeps {
showNoUpdateDialog: (version: string) => Promise<void>;
showUpdateAvailableDialog: (version: string) => Promise<'update' | 'close'>;
showUpdateFailedDialog: (message: string) => Promise<void>;
showManualUpdateRequiredDialog: (version: string) => Promise<void>;
downloadAppUpdate: () => Promise<void>;
showRestartDialog: () => Promise<'restart' | 'later'>;
quitAndInstall: () => void | Promise<void>;
@@ -158,8 +159,9 @@ export function createUpdateService(deps: UpdateServiceDeps) {
return { status: 'update-available', version: latest.version };
}
const canInstallAppUpdate = appUpdate.available && appUpdate.canUpdate !== false;
let appUpdateApplied = false;
if (appUpdate.available && appUpdate.canUpdate !== false) {
if (canInstallAppUpdate) {
await deps.downloadAppUpdate();
appUpdateApplied = true;
}
@@ -168,8 +170,8 @@ export function createUpdateService(deps: UpdateServiceDeps) {
deps.log(`Launcher update requires manual command: ${launcherResult.command}`);
}
const launcherUpdateApplied = launcherResult.status === 'updated';
if (!appUpdateApplied && !launcherUpdateApplied) {
if (!appUpdateApplied) {
await deps.showManualUpdateRequiredDialog(latest.version);
return { status: 'update-available', version: latest.version };
}