mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-17 13:13:35 +08:00
Update downloads never used the configured proxy because electron-updater runs all traffic on its own session partition, while the proxy was applied to app/defaultSession only. Point proxy config at autoUpdater.netSession, and make the system fallback an explicit `mode: 'system'` since an empty setProxy config means fixed_servers with no rules (= direct), which also silently downgraded the default session away from the OS proxy. Disable differential download: blockmap-driven ranged requests against the GitHub CDN are RTT-bound and run far below line speed on both macOS and Windows; full downloads restore expected bandwidth. Guard captureWindowState and the resize forwarder against destroyed windows: quitAndInstall tears the window down while late move/resize/close events still fire, crashing the main process with "Object has been destroyed". Widen the About page container from max-w-lg to max-w-2xl so release notes markdown is no longer cramped.
186 lines
7.5 KiB
TypeScript
186 lines
7.5 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest'
|
|
import { ElectronUpdaterService, normalizeUpdateInfo, updaterSessionProxyConfig, type ElectronUpdaterLike } from './updater'
|
|
import { mkdtempSync, rmSync } from 'node:fs'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
|
|
function fakeUpdater(): ElectronUpdaterLike & {
|
|
emitProgress(progress: { transferred?: number, total?: number }): void
|
|
checkForUpdates: ReturnType<typeof vi.fn>
|
|
downloadUpdate: ReturnType<typeof vi.fn>
|
|
quitAndInstall: ReturnType<typeof vi.fn>
|
|
} {
|
|
let progressHandler: ((progress: { transferred?: number, total?: number }) => void) | null = null
|
|
const updater = {
|
|
autoDownload: true,
|
|
checkForUpdates: vi.fn(),
|
|
downloadUpdate: vi.fn(),
|
|
quitAndInstall: vi.fn(),
|
|
on: vi.fn((_event, handler) => {
|
|
progressHandler = handler
|
|
return updater
|
|
}),
|
|
off: vi.fn((_event, handler) => {
|
|
if (progressHandler === handler) progressHandler = null
|
|
return updater
|
|
}),
|
|
emitProgress(progress) {
|
|
progressHandler?.(progress)
|
|
},
|
|
} as ElectronUpdaterLike & {
|
|
emitProgress(progress: { transferred?: number, total?: number }): void
|
|
checkForUpdates: ReturnType<typeof vi.fn>
|
|
downloadUpdate: ReturnType<typeof vi.fn>
|
|
quitAndInstall: ReturnType<typeof vi.fn>
|
|
}
|
|
return updater
|
|
}
|
|
|
|
const updater = fakeUpdater()
|
|
|
|
describe('Electron updater service', () => {
|
|
it('normalizes update metadata from electron-updater', () => {
|
|
expect(normalizeUpdateInfo({ version: '1.2.3', releaseNotes: 'Notes' })).toEqual({
|
|
version: '1.2.3',
|
|
body: 'Notes',
|
|
})
|
|
expect(normalizeUpdateInfo({ version: '1.2.4', releaseNotes: [{ note: 'One' }, { note: 'Two' }] })).toEqual({
|
|
version: '1.2.4',
|
|
body: 'One\n\nTwo',
|
|
})
|
|
expect(normalizeUpdateInfo(undefined)).toBeNull()
|
|
})
|
|
|
|
it('keeps electron-updater autoDownload disabled and emits store-compatible progress', async () => {
|
|
updater.checkForUpdates.mockResolvedValue({ updateInfo: { version: '1.2.3', body: 'Fixes' } })
|
|
updater.downloadUpdate.mockImplementation(async () => {
|
|
updater.emitProgress({ transferred: 40, total: 100 })
|
|
updater.emitProgress({ transferred: 100, total: 100 })
|
|
})
|
|
const service = new ElectronUpdaterService(updater)
|
|
const events: unknown[] = []
|
|
|
|
await expect(service.checkForUpdates()).resolves.toEqual({ version: '1.2.3', body: 'Fixes' })
|
|
await service.downloadUpdate(event => events.push(event))
|
|
|
|
expect(updater.autoDownload).toBe(false)
|
|
expect(updater.logger).toBeNull()
|
|
expect(events).toEqual([
|
|
{ event: 'Started', data: { contentLength: 100 } },
|
|
{ event: 'Progress', data: { chunkLength: 40 } },
|
|
{ event: 'Progress', data: { chunkLength: 60 } },
|
|
{ event: 'Finished' },
|
|
])
|
|
})
|
|
|
|
it('treats missing unpacked app update metadata as no update', async () => {
|
|
const service = new ElectronUpdaterService(updater)
|
|
updater.checkForUpdates.mockRejectedValueOnce(Object.assign(new Error("ENOENT: no such file or directory, open '/App/Contents/Resources/app-update.yml'"), {
|
|
code: 'ENOENT',
|
|
path: '/App/Contents/Resources/app-update.yml',
|
|
}))
|
|
|
|
await expect(service.checkForUpdates()).resolves.toBeNull()
|
|
})
|
|
|
|
it('skips electron-updater when packaged update config is absent', async () => {
|
|
const localUpdater = fakeUpdater()
|
|
const tempDir = mkdtempSync(join(tmpdir(), 'cc-haha-updater-'))
|
|
try {
|
|
const service = new ElectronUpdaterService(localUpdater, undefined, {
|
|
updateConfigPath: join(tempDir, 'app-update.yml'),
|
|
})
|
|
|
|
await expect(service.checkForUpdates()).resolves.toBeNull()
|
|
|
|
expect(localUpdater.checkForUpdates).not.toHaveBeenCalled()
|
|
} finally {
|
|
rmSync(tempDir, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
it('treats missing GitHub channel metadata as no update', async () => {
|
|
const service = new ElectronUpdaterService(updater)
|
|
updater.checkForUpdates.mockRejectedValueOnce(Object.assign(
|
|
new Error('Cannot find latest-mac.yml in the latest release artifacts (https://github.com/NanmiCoder/cc-haha/releases/download/v0.3.2/latest-mac.yml): HttpError: 404'),
|
|
{ code: 'ERR_UPDATER_CHANNEL_FILE_NOT_FOUND' },
|
|
))
|
|
|
|
await expect(service.checkForUpdates()).resolves.toBeNull()
|
|
})
|
|
|
|
it('treats missing GitHub channel metadata as no update even without an error code', async () => {
|
|
const service = new ElectronUpdaterService(updater)
|
|
updater.checkForUpdates.mockRejectedValueOnce(
|
|
new Error('Cannot find latest-mac.yml in the latest release artifacts (https://github.com/NanmiCoder/cc-haha/releases/download/v0.3.2/latest-mac.yml): HttpError: 404'),
|
|
)
|
|
|
|
await expect(service.checkForUpdates()).resolves.toBeNull()
|
|
})
|
|
|
|
it('treats stringified missing GitHub channel metadata as no update', async () => {
|
|
const service = new ElectronUpdaterService(updater)
|
|
updater.checkForUpdates.mockRejectedValueOnce(
|
|
'Error: Cannot find latest-mac.yml in the latest release artifacts (https://github.com/NanmiCoder/cc-haha/releases/download/v0.3.2/latest-mac.yml): HttpError: 404',
|
|
)
|
|
|
|
await expect(service.checkForUpdates()).resolves.toBeNull()
|
|
})
|
|
|
|
it('applies manual updater proxy before checking and clears it when returning to system proxy', async () => {
|
|
const localUpdater = fakeUpdater()
|
|
const proxyController = {
|
|
apply: vi.fn().mockResolvedValue(undefined),
|
|
}
|
|
localUpdater.checkForUpdates.mockResolvedValue({ updateInfo: { version: '1.2.5' } })
|
|
const service = new ElectronUpdaterService(localUpdater, proxyController)
|
|
|
|
await service.checkForUpdates({ proxy: 'http://127.0.0.1:7890' })
|
|
await service.checkForUpdates({ proxy: 'http://127.0.0.1:7890' })
|
|
await service.checkForUpdates()
|
|
|
|
expect(proxyController.apply).toHaveBeenNthCalledWith(1, 'http://127.0.0.1:7890')
|
|
expect(proxyController.apply).toHaveBeenNthCalledWith(2, null)
|
|
expect(proxyController.apply).toHaveBeenCalledTimes(2)
|
|
expect(localUpdater.checkForUpdates).toHaveBeenCalledTimes(3)
|
|
})
|
|
|
|
it('disables differential download so update downloads run at full bandwidth', () => {
|
|
const localUpdater = fakeUpdater()
|
|
|
|
void new ElectronUpdaterService(localUpdater)
|
|
|
|
expect(localUpdater.disableDifferentialDownload).toBe(true)
|
|
})
|
|
|
|
it('maps proxy settings to the updater net session proxy config', () => {
|
|
expect(updaterSessionProxyConfig(null)).toEqual({ mode: 'system' })
|
|
expect(updaterSessionProxyConfig('http://127.0.0.1:7890')).toEqual({
|
|
proxyRules: 'http://127.0.0.1:7890',
|
|
proxyBypassRules: '<local>',
|
|
})
|
|
})
|
|
|
|
it('does not hide non-metadata updater failures', async () => {
|
|
const service = new ElectronUpdaterService(updater)
|
|
updater.checkForUpdates.mockRejectedValueOnce(new Error('feed unavailable'))
|
|
|
|
await expect(service.checkForUpdates()).rejects.toThrow('feed unavailable')
|
|
})
|
|
|
|
it('stages then installs through quitAndInstall only after an update has downloaded', async () => {
|
|
const service = new ElectronUpdaterService(updater)
|
|
updater.checkForUpdates.mockResolvedValue({ updateInfo: { version: '1.2.4' } })
|
|
updater.downloadUpdate.mockResolvedValue(undefined)
|
|
|
|
expect(() => service.stageDownloadedUpdate()).toThrow('No Electron update is ready')
|
|
await service.checkForUpdates()
|
|
expect(() => service.stageDownloadedUpdate()).toThrow('has not finished downloading')
|
|
await service.downloadUpdate(() => {})
|
|
service.stageDownloadedUpdate()
|
|
service.quitAndInstallDownloadedUpdate()
|
|
|
|
expect(updater.quitAndInstall).toHaveBeenCalledWith(false, true)
|
|
})
|
|
})
|