Add signed launcher self-updates and publish Linux 0.1.3
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
import { equal, match } from 'node:assert/strict'
|
||||
import { test } from 'node:test'
|
||||
import { initialUpdaterState, updateBlocksOperations, updatePercent, updaterReducer } from './updater.ts'
|
||||
|
||||
const available = updaterReducer(initialUpdaterState, { type: 'loaded', checked: true,
|
||||
status: { currentVersion: '0.1.3', supported: true, version: '0.1.4' } })
|
||||
const downloading = updaterReducer(available, { type: 'install' })
|
||||
|
||||
test('checking and available update allow play; installation locks operations until restart', () => {
|
||||
equal(updateBlocksOperations(available), false)
|
||||
equal(updateBlocksOperations(updaterReducer(available, { type: 'check' })), false)
|
||||
equal(updateBlocksOperations(downloading), true)
|
||||
const ready = updaterReducer(downloading, { type: 'ready' })
|
||||
equal(updateBlocksOperations(ready), true)
|
||||
equal(updaterReducer(ready, { type: 'check' }), ready)
|
||||
equal(updaterReducer(ready, { type: 'install' }), ready)
|
||||
const restarting = updaterReducer(ready, { type: 'restart' })
|
||||
equal(updateBlocksOperations(restarting), true)
|
||||
})
|
||||
|
||||
test('an installation failure permits playing and retrying; a restart failure retains the lock', () => {
|
||||
const failed = updaterReducer(downloading, { type: 'failed', error: 'Подпись не совпадает' })
|
||||
equal(updateBlocksOperations(failed), false)
|
||||
equal(failed.status?.version, '0.1.4')
|
||||
const retry = updaterReducer(failed, { type: 'install' })
|
||||
equal(retry.error, null)
|
||||
equal(retry.phase, 'downloading')
|
||||
const ready = updaterReducer(retry, { type: 'ready' })
|
||||
const restartError = updaterReducer(updaterReducer(ready, { type: 'restart' }), { type: 'failed', error: 'Перезапустите вручную' })
|
||||
equal(restartError.phase, 'ready')
|
||||
match(restartError.error ?? '', /вручную/)
|
||||
equal(updateBlocksOperations(restartError), true)
|
||||
})
|
||||
|
||||
test('late progress cannot undo completion or a failed installation', () => {
|
||||
const installing = updaterReducer(downloading, { type: 'progress', progress: { stage: 'installing', downloadedBytes: 20 } })
|
||||
equal(updaterReducer(installing, { type: 'progress', progress: { stage: 'downloading', downloadedBytes: 10 } }), installing)
|
||||
const ready = updaterReducer(installing, { type: 'progress', progress: { stage: 'ready', downloadedBytes: 20 } })
|
||||
equal(updaterReducer(ready, { type: 'progress', progress: { stage: 'installing', downloadedBytes: 20 } }), ready)
|
||||
const failed = updaterReducer(downloading, { type: 'failed', error: 'Сбой сети' })
|
||||
equal(updaterReducer(failed, { type: 'ready' }), failed)
|
||||
equal(updaterReducer(failed, { type: 'progress', progress: { stage: 'ready', downloadedBytes: 20 } }), failed)
|
||||
})
|
||||
|
||||
test('native status restores an update after a webview reload; early events preserve readiness', () => {
|
||||
for (const stage of ['downloading', 'installing', 'ready'] as const) {
|
||||
const restored = updaterReducer(initialUpdaterState, { type: 'loaded', checked: false,
|
||||
status: { ...available.status!, stage } })
|
||||
equal(restored.phase, stage)
|
||||
equal(updateBlocksOperations(restored), true)
|
||||
}
|
||||
const earlyReady = updaterReducer(initialUpdaterState, { type: 'progress', progress: { stage: 'ready', downloadedBytes: 20 } })
|
||||
const staleStatus = updaterReducer(earlyReady, { type: 'loaded', checked: false,
|
||||
status: { ...available.status!, stage: 'downloading' } })
|
||||
equal(staleStatus.phase, 'ready')
|
||||
equal(staleStatus.status?.currentVersion, '0.1.3')
|
||||
})
|
||||
|
||||
test('unsupported package and no available version never enter installation', () => {
|
||||
const unsupported = updaterReducer(initialUpdaterState, { type: 'loaded', checked: false,
|
||||
status: { currentVersion: '0.1.3', supported: false, reason: 'Используйте AppImage' } })
|
||||
equal(updaterReducer(unsupported, { type: 'install' }), unsupported)
|
||||
const current = updaterReducer(initialUpdaterState, { type: 'loaded', checked: true,
|
||||
status: { currentVersion: '0.1.3', supported: true } })
|
||||
equal(updaterReducer(current, { type: 'install' }), current)
|
||||
})
|
||||
|
||||
test('unknown, invalid and excessive progress cannot produce misleading percentages', () => {
|
||||
equal(updatePercent(null), null)
|
||||
equal(updatePercent({ stage: 'downloading', downloadedBytes: 1 }), null)
|
||||
equal(updatePercent({ stage: 'downloading', downloadedBytes: NaN, totalBytes: 20 }), null)
|
||||
equal(updatePercent({ stage: 'downloading', downloadedBytes: 1, totalBytes: Infinity }), null)
|
||||
equal(updatePercent({ stage: 'downloading', downloadedBytes: 10, totalBytes: 20 }), 50)
|
||||
equal(updatePercent({ stage: 'downloading', downloadedBytes: 30, totalBytes: 20 }), 100)
|
||||
equal(updatePercent({ stage: 'downloading', downloadedBytes: -1, totalBytes: 20 }), 0)
|
||||
})
|
||||
@@ -0,0 +1,59 @@
|
||||
import type { LauncherUpdateProgress, LauncherUpdateStatus } from '../types/launcher'
|
||||
|
||||
export interface UpdaterState {
|
||||
phase: 'loading' | 'idle' | 'checking' | 'downloading' | 'installing' | 'ready' | 'restarting'
|
||||
status: LauncherUpdateStatus | null
|
||||
progress: LauncherUpdateProgress | null
|
||||
error: string | null
|
||||
checked: boolean
|
||||
}
|
||||
|
||||
export const initialUpdaterState: UpdaterState = {
|
||||
phase: 'loading', status: null, progress: null, error: null, checked: false,
|
||||
}
|
||||
|
||||
type UpdaterAction =
|
||||
| { type: 'loaded'; status: LauncherUpdateStatus; checked: boolean; error?: string | null }
|
||||
| { type: 'check' }
|
||||
| { type: 'install' }
|
||||
| { type: 'progress'; progress: LauncherUpdateProgress }
|
||||
| { type: 'ready' }
|
||||
| { type: 'restart' }
|
||||
| { type: 'failed'; error: string }
|
||||
|
||||
export function updateBlocksOperations(state: UpdaterState): boolean {
|
||||
return ['downloading', 'installing', 'ready', 'restarting'].includes(state.phase)
|
||||
}
|
||||
|
||||
export function updaterReducer(state: UpdaterState, action: UpdaterAction): UpdaterState {
|
||||
switch (action.type) {
|
||||
case 'loaded':
|
||||
if (updateBlocksOperations(state)) return { ...state, status: action.status }
|
||||
return { ...state, phase: action.status.stage === 'ready' || action.status.stage === 'downloading' || action.status.stage === 'installing'
|
||||
? action.status.stage : 'idle', status: action.status,
|
||||
checked: action.checked, error: action.error ?? null }
|
||||
case 'check':
|
||||
return updateBlocksOperations(state) ? state : { ...state, phase: 'checking', error: null }
|
||||
case 'install':
|
||||
return state.phase !== 'idle' || !state.status?.supported || !state.status.version ? state
|
||||
: { ...state, phase: 'downloading', progress: null, error: null }
|
||||
case 'progress':
|
||||
if (state.phase !== 'loading' && state.phase !== 'downloading' && state.phase !== 'installing') return state
|
||||
// A delayed download event cannot revert an installation to downloading.
|
||||
if (state.phase === 'installing' && action.progress.stage === 'downloading') return state
|
||||
return { ...state, phase: action.progress.stage, progress: action.progress }
|
||||
case 'ready':
|
||||
return state.phase === 'downloading' || state.phase === 'installing'
|
||||
? { ...state, phase: 'ready', error: null } : state
|
||||
case 'restart':
|
||||
return state.phase === 'ready' ? { ...state, phase: 'restarting', error: null } : state
|
||||
case 'failed':
|
||||
return { ...state, phase: state.phase === 'ready' || state.phase === 'restarting' ? 'ready' : 'idle', error: action.error }
|
||||
}
|
||||
}
|
||||
|
||||
export function updatePercent(progress: LauncherUpdateProgress | null): number | null {
|
||||
if (!progress || !progress.totalBytes || progress.totalBytes <= 0 ||
|
||||
!Number.isFinite(progress.totalBytes) || !Number.isFinite(progress.downloadedBytes)) return null
|
||||
return Math.max(0, Math.min(100, Math.round(progress.downloadedBytes / progress.totalBytes * 100)))
|
||||
}
|
||||
Reference in New Issue
Block a user