mirror of
https://github.com/stablyai/orca.git
synced 2026-09-30 16:02:56 +00:00
fix(updater): preserve Linux package recovery after checks
This commit is contained in:
@@ -99,6 +99,16 @@ const ARTIFACT = {
|
||||
path: '/home/tester/.cache/orca-updater/pending/orca-ide_1.0.61_amd64.deb',
|
||||
sha512: 'LHlL7dKoqg98gS2nfQv878dK+UoktbAkm4M20/hoJ2Qr0Kqsa3MSL4VmWy/Lll/MYjQFkpvOxduQ/vswentozA=='
|
||||
}
|
||||
const MANUAL_INSTALL_STATUS = {
|
||||
state: 'error',
|
||||
message: 'Quit Orca before running the system package install command.',
|
||||
recovery: {
|
||||
kind: 'linux-package-install',
|
||||
packageType: 'deb',
|
||||
reason: 'manual-install-required',
|
||||
version: '1.0.61'
|
||||
}
|
||||
} as const satisfies UpdateStatus
|
||||
|
||||
describe('linux package recovery actions', () => {
|
||||
afterEach(() => {
|
||||
@@ -199,6 +209,63 @@ describe('linux package recovery actions', () => {
|
||||
expect(showItemInFolderMock).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('restores recovery after a recheck resolves without a terminal event', async () => {
|
||||
const { send, updater } = await startUpdater()
|
||||
await activateRecovery(updater)
|
||||
send.mockClear()
|
||||
|
||||
updater.checkForUpdatesFromMenu()
|
||||
await vi.advanceTimersByTimeAsync(1_000)
|
||||
|
||||
expect(send).toHaveBeenLastCalledWith('updater:status', MANUAL_INSTALL_STATUS)
|
||||
await expect(updater.showLinuxPackage()).resolves.toBeUndefined()
|
||||
})
|
||||
|
||||
it('restores recovery after a recheck fails', async () => {
|
||||
const { send, updater } = await startUpdater()
|
||||
await activateRecovery(updater)
|
||||
autoUpdaterMock.checkForUpdates.mockRejectedValueOnce(new Error('offline'))
|
||||
send.mockClear()
|
||||
|
||||
updater.checkForUpdatesFromMenu()
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(send).toHaveBeenLastCalledWith('updater:status', MANUAL_INSTALL_STATUS)
|
||||
await expect(updater.getLinuxPackageInstallInstructions()).resolves.toEqual({
|
||||
ok: true,
|
||||
command: "sudo apt install -- '<pkg>'",
|
||||
packageFileName: 'p'
|
||||
})
|
||||
})
|
||||
|
||||
it('restores recovery when a pinned check resolves to the current version', async () => {
|
||||
const { send, updater } = await startUpdater()
|
||||
await activateRecovery(updater)
|
||||
send.mockClear()
|
||||
|
||||
updater.checkForUpdatesFromMenu({ channel: 'stable', targetTag: 'v1.0.51' })
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(send).toHaveBeenLastCalledWith('updater:status', MANUAL_INSTALL_STATUS)
|
||||
await expect(updater.showLinuxPackage()).resolves.toBeUndefined()
|
||||
})
|
||||
|
||||
it('restores recovery when resolving a pinned check fails', async () => {
|
||||
const { send, updater } = await startUpdater()
|
||||
await activateRecovery(updater)
|
||||
send.mockClear()
|
||||
|
||||
updater.checkForUpdatesFromMenu({ channel: 'stable', targetTag: 'not-a-release-tag' })
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(send).toHaveBeenLastCalledWith('updater:status', MANUAL_INSTALL_STATUS)
|
||||
await expect(updater.getLinuxPackageInstallInstructions()).resolves.toEqual({
|
||||
ok: true,
|
||||
command: "sudo apt install -- '<pkg>'",
|
||||
packageFileName: 'p'
|
||||
})
|
||||
})
|
||||
|
||||
it('replaces the structured status when revalidation fails so stale actions die', async () => {
|
||||
const { send, updater } = await startUpdater()
|
||||
await activateRecovery(updater)
|
||||
|
||||
+25
-12
@@ -39,6 +39,7 @@ import {
|
||||
getLinuxPackageType,
|
||||
LINUX_PACKAGE_MARKER_UNUSABLE_MESSAGE
|
||||
} from './linux-update-package-type'
|
||||
import { getRetainedLinuxPackageManualInstallStatus } from './linux-package-downloaded-status'
|
||||
import {
|
||||
createUpdaterDiagnosticLogger,
|
||||
redactLinuxPackageInstallText
|
||||
@@ -354,6 +355,17 @@ function sendStatus(status: UpdateStatus, options?: { force?: boolean }): void {
|
||||
mainWindowRef?.webContents.send('updater:status', decoratedStatus)
|
||||
}
|
||||
|
||||
function sendSettledCheckStatus(status: UpdateStatus): void {
|
||||
const retainedStatus = getRetainedLinuxPackageManualInstallStatus()
|
||||
if (retainedStatus) {
|
||||
sendStatus(retainedStatus)
|
||||
} else if (status.state === 'error') {
|
||||
sendErrorStatus(status.message, status.userInitiated)
|
||||
} else {
|
||||
sendStatus(status)
|
||||
}
|
||||
}
|
||||
|
||||
function getOptionsForUpdateCheckVariant(variant: UpdateCheckVariant): UpdateCheckOptions {
|
||||
switch (variant) {
|
||||
case 'perf':
|
||||
@@ -571,7 +583,7 @@ function settleSilentUpdateCheck(attemptId: number, userInitiated: boolean | und
|
||||
deferPendingUpdateNudgeUntilRetry()
|
||||
return
|
||||
}
|
||||
sendStatus({ state: 'not-available', userInitiated })
|
||||
sendSettledCheckStatus({ state: 'not-available', userInitiated })
|
||||
}
|
||||
}
|
||||
return
|
||||
@@ -581,7 +593,7 @@ function settleSilentUpdateCheck(attemptId: number, userInitiated: boolean | und
|
||||
backgroundCheckPromotedToUserInitiated = false
|
||||
userInitiatedCheck = false
|
||||
completeSilentUpdateCheck(userInitiated)
|
||||
sendStatus({ state: 'not-available', userInitiated })
|
||||
sendSettledCheckStatus({ state: 'not-available', userInitiated })
|
||||
}
|
||||
|
||||
function handleSettledUpdateCheckPromise(attemptId: number): void {
|
||||
@@ -994,7 +1006,7 @@ async function sendCheckFailureStatus(
|
||||
// error, or the pin blocks background checks for the process lifetime.
|
||||
clearAvailableUpdateContext()
|
||||
restoreReleaseUpdateSource()
|
||||
sendStatus({ state: 'error', message, userInitiated })
|
||||
sendSettledCheckStatus({ state: 'error', message, userInitiated })
|
||||
return
|
||||
}
|
||||
const failureKey = getCheckFailureKey(message, userInitiated)
|
||||
@@ -1039,18 +1051,19 @@ async function sendCheckFailureStatus(
|
||||
scheduleAutomaticUpdateCheck(AUTO_UPDATE_RETRY_INTERVAL_MS)
|
||||
if (userInitiated) {
|
||||
// Why: a user click needs visible feedback (idle looks broken); distinguish incomplete releases from transport failures.
|
||||
sendErrorStatus(
|
||||
isStableReleaseNotReadyFailure(sourceError)
|
||||
sendSettledCheckStatus({
|
||||
state: 'error',
|
||||
message: isStableReleaseNotReadyFailure(sourceError)
|
||||
? "A newer release isn't available for this device yet. Check again later."
|
||||
: "Couldn't reach the update server. Try again in a few minutes.",
|
||||
true
|
||||
)
|
||||
userInitiated: true
|
||||
})
|
||||
} else {
|
||||
if (isRetryableReleaseFeedPreflightFailure(sourceError)) {
|
||||
// Why: release probes can fail transiently; keep the campaign pending so the short retry can still show it.
|
||||
deferPendingUpdateNudgeUntilRetry()
|
||||
}
|
||||
sendStatus({ state: 'idle' })
|
||||
sendSettledCheckStatus({ state: 'idle' })
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -1060,7 +1073,7 @@ async function sendCheckFailureStatus(
|
||||
if (!userInitiated) {
|
||||
scheduleAutomaticUpdateCheck(AUTO_UPDATE_RETRY_INTERVAL_MS)
|
||||
}
|
||||
sendErrorStatus(message, userInitiated)
|
||||
sendSettledCheckStatus({ state: 'error', message, userInitiated })
|
||||
}
|
||||
|
||||
pendingCheckFailureKey = failureKey
|
||||
@@ -1585,7 +1598,7 @@ export function checkForUpdatesFromMenu(options?: UpdateCheckOptions): void {
|
||||
userInitiatedCheck = false
|
||||
finishActiveUpdateCheckAttempt()
|
||||
recordCompletedUpdateCheck()
|
||||
sendStatus({ state: 'not-available', userInitiated: true })
|
||||
sendSettledCheckStatus({ state: 'not-available', userInitiated: true })
|
||||
return false
|
||||
}
|
||||
return launch()
|
||||
@@ -1712,7 +1725,7 @@ async function checkForPinnedBuild(channel: ReleaseChannel, tag: string): Promis
|
||||
try {
|
||||
const target = resolveTargetBuild(channel, tag)
|
||||
if (compareVersions(target.version, app.getVersion()) === 0) {
|
||||
sendStatus({ state: 'not-available', userInitiated: true })
|
||||
sendSettledCheckStatus({ state: 'not-available', userInitiated: true })
|
||||
return
|
||||
}
|
||||
closeLocalBuildFeed()
|
||||
@@ -1741,7 +1754,7 @@ async function checkForPinnedBuild(channel: ReleaseChannel, tag: string): Promis
|
||||
userInitiatedCheck = false
|
||||
clearAvailableUpdateContext()
|
||||
restoreReleaseUpdateSource()
|
||||
sendStatus({
|
||||
sendSettledCheckStatus({
|
||||
state: 'error',
|
||||
message: String((error as Error)?.message ?? error),
|
||||
userInitiated: true
|
||||
|
||||
Reference in New Issue
Block a user