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
720 lines
27 KiB
TypeScript
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'
|
|
})
|
|
})
|
|
})
|