Files
orca/src/main/updater.prerelease-fallback.test.ts
T
mirror 34fa2cd5ca
Skill update round trip / roundtrip (false, macos-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, macos-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, ubuntu-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, ubuntu-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, ubuntu-latest, symlink, latest) (push) Canceled after 0s
Skill update round trip / roundtrip (false, windows-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, windows-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, macos-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, macos-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, ubuntu-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, ubuntu-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, windows-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, windows-latest, symlink, 1.5.17) (push) Canceled after 0s
source snapshot 2026-09-02
2026-09-02 02:03:16 +08:00

720 lines
27 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest'
import { loadUpdaterModule, warmUpdaterModule } from './updater-test-module-loader'
const { appMock, autoUpdaterMock, fetchNewerReleaseTagsMock, moduleFactories, resetUpdaterMocks } =
await vi.hoisted(async () => (await import('./updater-test-harness')).createUpdaterMocks())
vi.mock('electron', () => moduleFactories.electron())
vi.mock('electron-updater', () => moduleFactories.electronUpdater())
vi.mock('./electron-updater-loader', () => moduleFactories.electronUpdaterLoader())
vi.mock('@electron-toolkit/utils', () => moduleFactories.electronToolkitUtils())
vi.mock('./ipc/pty', () => moduleFactories.ipcPty())
vi.mock('./linux-update-package-type', () => moduleFactories.linuxUpdatePackageType())
vi.mock('./updater-lifecycle-diagnostics', () => moduleFactories.updaterLifecycleDiagnostics())
vi.mock('./updater-changelog', () => moduleFactories.updaterChangelog())
vi.mock('./updater-nudge', () => moduleFactories.updaterNudge())
vi.mock('./update-install-exit-watchdog', () => moduleFactories.updateInstallExitWatchdog())
vi.mock('./updater-prerelease-feed', () => moduleFactories.updaterPrereleaseFeed())
vi.mock('./local-builds/local-build-switch', () => moduleFactories.localBuildSwitch())
vi.mock('./local-builds/local-build-feed-server', () => moduleFactories.localBuildFeedServer())
warmUpdaterModule()
describe('updater', () => {
beforeEach(() => {
resetUpdaterMocks()
})
it('retries a prerelease check once against the previous feed tag when the manifest is missing', async () => {
appMock.getVersion.mockReturnValue('1.3.51-rc.6')
fetchNewerReleaseTagsMock.mockResolvedValue(['v1.3.51-rc.7', 'v1.3.51-rc.6'])
const missingManifest = new Error(
'Cannot find channel "latest-mac.yml" update info: HttpError: 404'
)
autoUpdaterMock.checkForUpdates.mockImplementation(() => {
autoUpdaterMock.emit('checking-for-update')
if (autoUpdaterMock.checkForUpdates.mock.calls.length === 1) {
queueMicrotask(() => {
autoUpdaterMock.emit('error', missingManifest)
})
return Promise.reject(missingManifest)
}
queueMicrotask(() => {
autoUpdaterMock.emit('update-not-available')
})
return Promise.resolve(undefined)
})
const sendMock = vi.fn()
const mainWindow = { webContents: { send: sendMock } }
const { setupAutoUpdater, checkForUpdatesFromMenu } = await loadUpdaterModule()
setupAutoUpdater(mainWindow as never, { getLastUpdateCheckAt: () => Date.now() })
checkForUpdatesFromMenu()
await vi.waitFor(() => {
expect(autoUpdaterMock.checkForUpdates).toHaveBeenCalledTimes(2)
expect(autoUpdaterMock.setFeedURL).toHaveBeenCalledWith({
provider: 'generic',
url: 'https://github.com/stablyai/orca/releases/download/v1.3.51-rc.7'
})
expect(autoUpdaterMock.setFeedURL).toHaveBeenLastCalledWith({
provider: 'generic',
url: 'https://github.com/stablyai/orca/releases/download/v1.3.51-rc.6'
})
})
const statuses = sendMock.mock.calls
.filter(([channel]) => channel === 'updater:status')
.map(([, status]) => status)
expect(statuses).toContainEqual({ state: 'not-available', userInitiated: true })
expect(statuses).not.toContainEqual(expect.objectContaining({ state: 'error' }))
})
it('surfaces a promise-only prerelease fallback failure after the primary error event', async () => {
appMock.getVersion.mockReturnValue('1.3.51-rc.6')
fetchNewerReleaseTagsMock.mockResolvedValue(['v1.3.51-rc.7', 'v1.3.51-rc.6'])
const missingManifest = new Error(
'Cannot find channel "latest-mac.yml" update info: HttpError: 404'
)
autoUpdaterMock.checkForUpdates.mockImplementation(() => {
autoUpdaterMock.emit('checking-for-update')
if (autoUpdaterMock.checkForUpdates.mock.calls.length === 1) {
queueMicrotask(() => {
autoUpdaterMock.emit('error', missingManifest)
})
return new Promise(() => {})
}
return Promise.reject(missingManifest)
})
const sendMock = vi.fn()
const mainWindow = { webContents: { send: sendMock } }
const { setupAutoUpdater, checkForUpdatesFromMenu } = await loadUpdaterModule()
setupAutoUpdater(mainWindow as never, { getLastUpdateCheckAt: () => Date.now() })
checkForUpdatesFromMenu()
await vi.waitFor(() => {
expect(autoUpdaterMock.checkForUpdates).toHaveBeenCalledTimes(2)
const statuses = sendMock.mock.calls
.filter(([channel]) => channel === 'updater:status')
.map(([, status]) => status)
expect(statuses).toContainEqual({
state: 'error',
message: "Couldn't reach the update server. Try again in a few minutes.",
userInitiated: true
})
})
})
it('allows the short background retry to launch after a promise-only prerelease fallback failure', async () => {
vi.useFakeTimers()
vi.setSystemTime(new Date('2026-04-03T12:00:00Z'))
appMock.getVersion.mockReturnValue('1.3.51-rc.6')
fetchNewerReleaseTagsMock.mockResolvedValue(['v1.3.51-rc.7', 'v1.3.51-rc.6'])
const missingManifest = new Error(
'Cannot find channel "latest-mac.yml" update info: HttpError: 404'
)
autoUpdaterMock.checkForUpdates.mockImplementation(() => {
const callCount = autoUpdaterMock.checkForUpdates.mock.calls.length
if (callCount === 1) {
autoUpdaterMock.emit('checking-for-update')
queueMicrotask(() => {
autoUpdaterMock.emit('error', missingManifest)
})
return new Promise(() => {})
}
if (callCount === 2) {
return Promise.reject(missingManifest)
}
return new Promise(() => {})
})
const sendMock = vi.fn()
const mainWindow = { webContents: { send: sendMock } }
const { setupAutoUpdater } = await loadUpdaterModule()
setupAutoUpdater(mainWindow as never, { getLastUpdateCheckAt: () => null })
await vi.waitFor(() => {
expect(autoUpdaterMock.checkForUpdates).toHaveBeenCalledTimes(2)
const statuses = sendMock.mock.calls
.filter(([channel]) => channel === 'updater:status')
.map(([, status]) => status)
expect(statuses).toContainEqual({ state: 'idle' })
})
await vi.advanceTimersByTimeAsync(59 * 60 * 1000)
expect(autoUpdaterMock.checkForUpdates).toHaveBeenCalledTimes(2)
await vi.advanceTimersByTimeAsync(60 * 1000)
expect(autoUpdaterMock.checkForUpdates).toHaveBeenCalledTimes(3)
})
it('does not let user-initiated promise-only fallback failures taint the next background check', async () => {
let lastUpdateCheckAt = Date.now()
appMock.getVersion.mockReturnValue('1.3.51-rc.6')
fetchNewerReleaseTagsMock.mockResolvedValue(['v1.3.51-rc.7', 'v1.3.51-rc.6'])
const missingManifest = new Error(
'Cannot find channel "latest-mac.yml" update info: HttpError: 404'
)
autoUpdaterMock.checkForUpdates.mockImplementation(() => {
const callCount = autoUpdaterMock.checkForUpdates.mock.calls.length
if (callCount === 1) {
autoUpdaterMock.emit('checking-for-update')
queueMicrotask(() => {
autoUpdaterMock.emit('error', missingManifest)
})
return new Promise(() => {})
}
if (callCount === 2) {
return Promise.reject(missingManifest)
}
autoUpdaterMock.emit('checking-for-update')
queueMicrotask(() => {
autoUpdaterMock.emit('update-not-available')
})
return Promise.resolve(undefined)
})
const sendMock = vi.fn()
const mainWindow = { webContents: { send: sendMock } }
const { setupAutoUpdater, checkForUpdatesFromMenu } = await loadUpdaterModule()
setupAutoUpdater(mainWindow as never, { getLastUpdateCheckAt: () => lastUpdateCheckAt })
checkForUpdatesFromMenu()
await vi.waitFor(() => {
const statuses = sendMock.mock.calls
.filter(([channel]) => channel === 'updater:status')
.map(([, status]) => status)
expect(statuses).toContainEqual({
state: 'error',
message: "Couldn't reach the update server. Try again in a few minutes.",
userInitiated: true
})
})
sendMock.mockClear()
lastUpdateCheckAt = Date.now() - 25 * 60 * 60 * 1000
appMock.emit('browser-window-focus')
await vi.waitFor(() => {
expect(autoUpdaterMock.checkForUpdates).toHaveBeenCalledTimes(3)
const statuses = sendMock.mock.calls
.filter(([channel]) => channel === 'updater:status')
.map(([, status]) => status)
expect(statuses).toContainEqual({ state: 'not-available' })
expect(statuses).not.toContainEqual({ state: 'checking', userInitiated: true })
expect(statuses).not.toContainEqual({ state: 'not-available', userInitiated: true })
})
})
it('preserves user-initiated state for delayed prerelease fallback not-available', async () => {
vi.useFakeTimers()
appMock.getVersion.mockReturnValue('1.3.51-rc.6')
fetchNewerReleaseTagsMock.mockResolvedValue(['v1.3.51-rc.7', 'v1.3.51-rc.6'])
const missingManifest = new Error(
'Cannot find channel "latest-mac.yml" update info: HttpError: 404'
)
autoUpdaterMock.checkForUpdates.mockImplementation(() => {
const callCount = autoUpdaterMock.checkForUpdates.mock.calls.length
if (callCount === 1) {
autoUpdaterMock.emit('checking-for-update')
queueMicrotask(() => {
autoUpdaterMock.emit('error', missingManifest)
})
return Promise.reject(missingManifest)
}
setTimeout(() => {
autoUpdaterMock.emit('update-not-available')
}, 10)
return Promise.resolve(undefined)
})
const sendMock = vi.fn()
const mainWindow = { webContents: { send: sendMock } }
const { setupAutoUpdater, checkForUpdatesFromMenu } = await loadUpdaterModule()
setupAutoUpdater(mainWindow as never, { getLastUpdateCheckAt: () => Date.now() })
checkForUpdatesFromMenu()
await vi.waitFor(() => {
expect(autoUpdaterMock.checkForUpdates).toHaveBeenCalledTimes(2)
})
await vi.advanceTimersByTimeAsync(10)
const statuses = sendMock.mock.calls
.filter(([channel]) => channel === 'updater:status')
.map(([, status]) => status)
expect(statuses).toContainEqual({ state: 'not-available', userInitiated: true })
})
it('ignores a delayed primary error after a promise-launched prerelease fallback', async () => {
vi.useFakeTimers()
vi.setSystemTime(new Date('2026-04-03T12:00:00Z'))
appMock.getVersion.mockReturnValue('1.3.51-rc.6')
fetchNewerReleaseTagsMock.mockResolvedValue(['v1.3.51-rc.7', 'v1.3.51-rc.6'])
const missingManifest = new Error(
'Cannot find channel "latest-mac.yml" update info: HttpError: 404'
)
autoUpdaterMock.checkForUpdates.mockImplementation(() => {
const callCount = autoUpdaterMock.checkForUpdates.mock.calls.length
autoUpdaterMock.emit('checking-for-update')
if (callCount === 1) {
setTimeout(() => {
autoUpdaterMock.emit('error', missingManifest)
}, 10)
return Promise.reject(missingManifest)
}
if (callCount === 2) {
setTimeout(() => {
autoUpdaterMock.emit('update-not-available')
}, 20)
return Promise.resolve(undefined)
}
return new Promise(() => {})
})
const sendMock = vi.fn()
const setLastUpdateCheckAt = vi.fn()
const mainWindow = { webContents: { send: sendMock } }
const { setupAutoUpdater } = await loadUpdaterModule()
setupAutoUpdater(mainWindow as never, {
getLastUpdateCheckAt: () => null,
setLastUpdateCheckAt
})
await vi.waitFor(() => {
expect(autoUpdaterMock.checkForUpdates).toHaveBeenCalledTimes(2)
})
await vi.advanceTimersByTimeAsync(30)
const statuses = sendMock.mock.calls
.filter(([channel]) => channel === 'updater:status')
.map(([, status]) => status)
expect(statuses).toContainEqual({ state: 'not-available' })
expect(statuses).not.toContainEqual(expect.objectContaining({ state: 'error' }))
expect(setLastUpdateCheckAt).not.toHaveBeenCalled()
await vi.advanceTimersByTimeAsync(59 * 60 * 1000)
expect(autoUpdaterMock.checkForUpdates).toHaveBeenCalledTimes(2)
await vi.advanceTimersByTimeAsync(60 * 1000)
expect(autoUpdaterMock.checkForUpdates).toHaveBeenCalledTimes(3)
})
it('handles an event-only fallback error after a promise-only primary failure', async () => {
appMock.getVersion.mockReturnValue('1.3.51-rc.6')
fetchNewerReleaseTagsMock.mockResolvedValue(['v1.3.51-rc.7', 'v1.3.51-rc.6'])
const missingManifestMessage =
'Cannot find channel "latest-mac.yml" update info: HttpError: 404'
const primaryMissingManifest = new Error(missingManifestMessage)
const fallbackMissingManifest = new Error(missingManifestMessage)
autoUpdaterMock.checkForUpdates.mockImplementation(() => {
const callCount = autoUpdaterMock.checkForUpdates.mock.calls.length
autoUpdaterMock.emit('checking-for-update')
if (callCount === 1) {
return Promise.reject(primaryMissingManifest)
}
queueMicrotask(() => {
autoUpdaterMock.emit('error', fallbackMissingManifest)
})
return new Promise(() => {})
})
const sendMock = vi.fn()
const mainWindow = { webContents: { send: sendMock } }
const { setupAutoUpdater, checkForUpdatesFromMenu } = await loadUpdaterModule()
setupAutoUpdater(mainWindow as never, { getLastUpdateCheckAt: () => Date.now() })
checkForUpdatesFromMenu()
await vi.waitFor(() => {
expect(autoUpdaterMock.checkForUpdates).toHaveBeenCalledTimes(2)
const statuses = sendMock.mock.calls
.filter(([channel]) => channel === 'updater:status')
.map(([, status]) => status)
expect(statuses.at(-1)).toEqual({
state: 'error',
message: "Couldn't reach the update server. Try again in a few minutes.",
userInitiated: true
})
})
})
it('suppresses a delayed background fallback error after the fallback promise handled it', async () => {
vi.useFakeTimers()
vi.setSystemTime(new Date('2026-04-03T12:00:00Z'))
appMock.getVersion.mockReturnValue('1.3.51-rc.6')
fetchNewerReleaseTagsMock.mockResolvedValue(['v1.3.51-rc.7', 'v1.3.51-rc.6'])
const missingManifest = new Error(
'Cannot find channel "latest-mac.yml" update info: HttpError: 404'
)
autoUpdaterMock.checkForUpdates.mockImplementation(() => {
const callCount = autoUpdaterMock.checkForUpdates.mock.calls.length
autoUpdaterMock.emit('checking-for-update')
if (callCount === 1) {
queueMicrotask(() => {
autoUpdaterMock.emit('error', missingManifest)
})
return new Promise(() => {})
}
if (callCount === 2) {
setTimeout(() => {
autoUpdaterMock.emit('error', missingManifest)
}, 10)
return Promise.reject(missingManifest)
}
return new Promise(() => {})
})
const sendMock = vi.fn()
const mainWindow = { webContents: { send: sendMock } }
const { setupAutoUpdater } = await loadUpdaterModule()
setupAutoUpdater(mainWindow as never, { getLastUpdateCheckAt: () => null })
await vi.waitFor(() => {
expect(autoUpdaterMock.checkForUpdates).toHaveBeenCalledTimes(2)
const statuses = sendMock.mock.calls
.filter(([channel]) => channel === 'updater:status')
.map(([, status]) => status)
expect(statuses).toContainEqual({ state: 'idle' })
})
sendMock.mockClear()
await vi.advanceTimersByTimeAsync(10)
const statusesAfterLateError = sendMock.mock.calls
.filter(([channel]) => channel === 'updater:status')
.map(([, status]) => status)
expect(statusesAfterLateError).not.toContainEqual(
expect.objectContaining({ state: 'error', message: missingManifest.message })
)
await vi.advanceTimersByTimeAsync(59 * 60 * 1000)
expect(autoUpdaterMock.checkForUpdates).toHaveBeenCalledTimes(2)
await vi.advanceTimersByTimeAsync(60 * 1000)
expect(autoUpdaterMock.checkForUpdates).toHaveBeenCalledTimes(3)
})
it('suppresses a delayed user fallback error after the fallback promise handled it', async () => {
vi.useFakeTimers()
appMock.getVersion.mockReturnValue('1.3.51-rc.6')
fetchNewerReleaseTagsMock.mockResolvedValue(['v1.3.51-rc.7', 'v1.3.51-rc.6'])
const missingManifest = new Error(
'Cannot find channel "latest-mac.yml" update info: HttpError: 404'
)
autoUpdaterMock.checkForUpdates.mockImplementation(() => {
const callCount = autoUpdaterMock.checkForUpdates.mock.calls.length
autoUpdaterMock.emit('checking-for-update')
if (callCount === 1) {
queueMicrotask(() => {
autoUpdaterMock.emit('error', missingManifest)
})
return new Promise(() => {})
}
setTimeout(() => {
autoUpdaterMock.emit('error', missingManifest)
}, 10)
return Promise.reject(missingManifest)
})
const sendMock = vi.fn()
const mainWindow = { webContents: { send: sendMock } }
const { setupAutoUpdater, checkForUpdatesFromMenu } = await loadUpdaterModule()
setupAutoUpdater(mainWindow as never, { getLastUpdateCheckAt: () => Date.now() })
checkForUpdatesFromMenu()
await vi.waitFor(() => {
const statuses = sendMock.mock.calls
.filter(([channel]) => channel === 'updater:status')
.map(([, status]) => status)
expect(statuses).toContainEqual({
state: 'error',
message: "Couldn't reach the update server. Try again in a few minutes.",
userInitiated: true
})
})
sendMock.mockClear()
await vi.advanceTimersByTimeAsync(10)
const statusesAfterLateError = sendMock.mock.calls
.filter(([channel]) => channel === 'updater:status')
.map(([, status]) => status)
expect(statusesAfterLateError).not.toContainEqual(
expect.objectContaining({ state: 'error', message: missingManifest.message })
)
})
it('keeps background prerelease fallback not-available on the short retry cadence', async () => {
vi.useFakeTimers()
vi.setSystemTime(new Date('2026-04-03T12:00:00Z'))
appMock.getVersion.mockReturnValue('1.3.51-rc.6')
fetchNewerReleaseTagsMock.mockResolvedValue(['v1.3.51-rc.7', 'v1.3.51-rc.6'])
const missingManifest = new Error(
'Cannot find channel "latest-mac.yml" update info: HttpError: 404'
)
autoUpdaterMock.checkForUpdates.mockImplementation(() => {
autoUpdaterMock.emit('checking-for-update')
if (autoUpdaterMock.checkForUpdates.mock.calls.length === 1) {
queueMicrotask(() => {
autoUpdaterMock.emit('error', missingManifest)
})
return new Promise(() => {})
}
if (autoUpdaterMock.checkForUpdates.mock.calls.length === 2) {
queueMicrotask(() => {
autoUpdaterMock.emit('update-not-available')
})
return Promise.resolve(undefined)
}
return new Promise(() => {})
})
const sendMock = vi.fn()
const setLastUpdateCheckAt = vi.fn()
const mainWindow = { webContents: { send: sendMock } }
const { setupAutoUpdater } = await loadUpdaterModule()
setupAutoUpdater(mainWindow as never, {
getLastUpdateCheckAt: () => null,
setLastUpdateCheckAt
})
await vi.waitFor(() => {
expect(autoUpdaterMock.checkForUpdates).toHaveBeenCalledTimes(2)
const statuses = sendMock.mock.calls
.filter(([channel]) => channel === 'updater:status')
.map(([, status]) => status)
expect(statuses).toContainEqual({ state: 'not-available' })
})
expect(setLastUpdateCheckAt).not.toHaveBeenCalled()
await vi.advanceTimersByTimeAsync(59 * 60 * 1000)
expect(autoUpdaterMock.checkForUpdates).toHaveBeenCalledTimes(2)
await vi.advanceTimersByTimeAsync(60 * 1000)
expect(autoUpdaterMock.checkForUpdates).toHaveBeenCalledTimes(3)
})
it('keeps user prerelease fallback not-available on the short retry cadence', async () => {
vi.useFakeTimers()
vi.setSystemTime(new Date('2026-04-03T12:00:00Z'))
appMock.getVersion.mockReturnValue('1.3.51-rc.6')
fetchNewerReleaseTagsMock.mockResolvedValue(['v1.3.51-rc.7', 'v1.3.51-rc.6'])
const missingManifest = new Error(
'Cannot find channel "latest-mac.yml" update info: HttpError: 404'
)
autoUpdaterMock.checkForUpdates.mockImplementation(() => {
const callCount = autoUpdaterMock.checkForUpdates.mock.calls.length
autoUpdaterMock.emit('checking-for-update')
if (callCount === 1) {
queueMicrotask(() => {
autoUpdaterMock.emit('error', missingManifest)
})
return new Promise(() => {})
}
if (callCount === 2) {
queueMicrotask(() => {
autoUpdaterMock.emit('update-not-available')
})
return Promise.resolve(undefined)
}
return new Promise(() => {})
})
const sendMock = vi.fn()
const setLastUpdateCheckAt = vi.fn()
const mainWindow = { webContents: { send: sendMock } }
const { setupAutoUpdater, checkForUpdatesFromMenu } = await loadUpdaterModule()
setupAutoUpdater(mainWindow as never, {
getLastUpdateCheckAt: () => Date.now(),
setLastUpdateCheckAt
})
checkForUpdatesFromMenu()
await vi.waitFor(() => {
expect(autoUpdaterMock.checkForUpdates).toHaveBeenCalledTimes(2)
const statuses = sendMock.mock.calls
.filter(([channel]) => channel === 'updater:status')
.map(([, status]) => status)
expect(statuses).toContainEqual({ state: 'not-available', userInitiated: true })
})
expect(setLastUpdateCheckAt).not.toHaveBeenCalled()
await vi.advanceTimersByTimeAsync(59 * 60 * 1000)
expect(autoUpdaterMock.checkForUpdates).toHaveBeenCalledTimes(2)
await vi.advanceTimersByTimeAsync(60 * 1000)
expect(autoUpdaterMock.checkForUpdates).toHaveBeenCalledTimes(3)
})
it('keeps user prerelease fallback available on the short retry cadence', async () => {
vi.useFakeTimers()
vi.setSystemTime(new Date('2026-04-03T12:00:00Z'))
appMock.getVersion.mockReturnValue('1.3.51-rc.5')
fetchNewerReleaseTagsMock.mockResolvedValue(['v1.3.51-rc.7', 'v1.3.51-rc.6'])
const missingManifest = new Error(
'Cannot find channel "latest-mac.yml" update info: HttpError: 404'
)
autoUpdaterMock.checkForUpdates.mockImplementation(() => {
const callCount = autoUpdaterMock.checkForUpdates.mock.calls.length
autoUpdaterMock.emit('checking-for-update')
if (callCount === 1) {
queueMicrotask(() => {
autoUpdaterMock.emit('error', missingManifest)
})
return new Promise(() => {})
}
if (callCount === 2) {
queueMicrotask(() => {
autoUpdaterMock.emit('update-available', { version: '1.3.51-rc.6' })
})
return Promise.resolve(undefined)
}
return new Promise(() => {})
})
const sendMock = vi.fn()
const setLastUpdateCheckAt = vi.fn()
const mainWindow = { webContents: { send: sendMock } }
const { setupAutoUpdater, checkForUpdatesFromMenu } = await loadUpdaterModule()
setupAutoUpdater(mainWindow as never, {
getLastUpdateCheckAt: () => Date.now(),
setLastUpdateCheckAt
})
checkForUpdatesFromMenu()
await vi.waitFor(() => {
expect(autoUpdaterMock.checkForUpdates).toHaveBeenCalledTimes(2)
const statuses = sendMock.mock.calls
.filter(([channel]) => channel === 'updater:status')
.map(([, status]) => status)
expect(statuses).toContainEqual({
state: 'available',
version: '1.3.51-rc.6',
changelog: null
})
})
expect(setLastUpdateCheckAt).not.toHaveBeenCalled()
await vi.advanceTimersByTimeAsync(59 * 60 * 1000)
expect(autoUpdaterMock.checkForUpdates).toHaveBeenCalledTimes(2)
await vi.advanceTimersByTimeAsync(60 * 1000)
expect(autoUpdaterMock.checkForUpdates).toHaveBeenCalledTimes(3)
})
it('surfaces the failure when the bounded prerelease fallback also misses its manifest', async () => {
appMock.getVersion.mockReturnValue('1.3.51-rc.6')
fetchNewerReleaseTagsMock.mockResolvedValue(['v1.3.51-rc.7', 'v1.3.51-rc.6'])
const missingManifest = new Error(
'Cannot find channel "latest-mac.yml" update info: HttpError: 404'
)
autoUpdaterMock.checkForUpdates.mockImplementation(() => {
autoUpdaterMock.emit('checking-for-update')
queueMicrotask(() => {
autoUpdaterMock.emit('error', missingManifest)
})
return Promise.reject(missingManifest)
})
const sendMock = vi.fn()
const mainWindow = { webContents: { send: sendMock } }
const { setupAutoUpdater, checkForUpdatesFromMenu } = await loadUpdaterModule()
setupAutoUpdater(mainWindow as never, { getLastUpdateCheckAt: () => Date.now() })
checkForUpdatesFromMenu()
await vi.waitFor(() => {
expect(autoUpdaterMock.checkForUpdates).toHaveBeenCalledTimes(2)
})
await vi.waitFor(() => {
const statuses = sendMock.mock.calls
.filter(([channel]) => channel === 'updater:status')
.map(([, status]) => status)
expect(statuses).toContainEqual({
state: 'error',
message: "Couldn't reach the update server. Try again in a few minutes.",
userInitiated: true
})
})
})
// Why: /releases/latest/download is a moving redirect; a relative ZIP URL from an old manifest can resolve against a newer release and 404.
it('pins the generic feed to a concrete stable tag for a stable user', async () => {
appMock.getVersion.mockReturnValue('1.3.17')
fetchNewerReleaseTagsMock.mockResolvedValue(['v1.3.18'])
autoUpdaterMock.checkForUpdates.mockResolvedValue(undefined)
const { setupAutoUpdater, checkForUpdatesFromMenu } = await loadUpdaterModule()
const mainWindow = { webContents: { send: vi.fn() } }
setupAutoUpdater(mainWindow as never, { getLastUpdateCheckAt: () => Date.now() })
checkForUpdatesFromMenu()
await vi.waitFor(() => {
expect(fetchNewerReleaseTagsMock).toHaveBeenCalledWith('1.3.17', 1, {
includePrerelease: false
})
expect(autoUpdaterMock.checkForUpdates).toHaveBeenCalledTimes(1)
})
expect(autoUpdaterMock.setFeedURL).toHaveBeenLastCalledWith({
provider: 'generic',
url: 'https://github.com/stablyai/orca/releases/download/v1.3.18'
})
})
// Why: native GitHub provider can pick cancelled prerelease tags with missing manifests, so keep the manifest-probed generic feed.
it('uses the manifest-probed generic feed after a Shift-click RC opt-in', async () => {
appMock.getVersion.mockReturnValue('1.3.17')
fetchNewerReleaseTagsMock.mockResolvedValue(['v1.3.18-rc.1'])
autoUpdaterMock.checkForUpdates.mockResolvedValue(undefined)
const { setupAutoUpdater, checkForUpdatesFromMenu } = await loadUpdaterModule()
const mainWindow = { webContents: { send: vi.fn() } }
setupAutoUpdater(mainWindow as never, { getLastUpdateCheckAt: () => Date.now() })
checkForUpdatesFromMenu({ includePrerelease: true })
await vi.waitFor(() => {
expect(fetchNewerReleaseTagsMock).toHaveBeenCalledWith('1.3.17', 2, {
includePrerelease: true
})
expect(autoUpdaterMock.checkForUpdates).toHaveBeenCalledTimes(1)
})
expect(autoUpdaterMock.allowPrerelease).toBe(true)
expect(autoUpdaterMock.setFeedURL).toHaveBeenLastCalledWith({
provider: 'generic',
url: 'https://github.com/stablyai/orca/releases/download/v1.3.18-rc.1'
})
})
})