import { beforeEach, describe, expect, it, vi } from 'vitest' import type { UpdateConfig } from '../../src/main/types/config.schema' import type { UpdateCatalog, UpdateRelease, UpdateStatus } from '../../src/main/types/update.types' const mockPublishUpdateStatus = vi.fn() const mockReadText = vi.fn() const mockDownloadToFile = vi.fn() const mockLoadCatalog = vi.fn() const mockGetDialogCatalog = vi.fn() const mockResolveUserStatus = vi.fn() const mockResolveAdminStatus = vi.fn() const mockGetDownloadPath = vi.fn() const mockCalculateSha256 = vi.fn() const mockInstallDownloadedRelease = vi.fn() const mockGetValidDownloadedRelease = vi.fn() let currentUpdateConfig: UpdateConfig = { enabled: true, allowDevMode: true, endpoint: 'http://localhost:9000', accessKey: 'key', secretKey: 'secret', bucket: 'bucket', region: 'us-east-1', basePrefix: 'updates/win-portable', checkIntervalMinutes: 30, maxAdminHistoryPerChannel: 10 } function createRelease(version: string, channel: 'stable' | 'preview' = 'stable'): UpdateRelease { return { version, channel, artifactKey: `${channel}/${version}.exe`, sha256: `${channel}-${version}-sha`, size: 1, publishedAt: '2026-03-21T10:00:00Z', changelogKey: `${channel}/${version}.md` } } vi.mock('../../src/main/services/config/config-manager', () => ({ ConfigManager: { getInstance: vi.fn(() => ({ getConfig: () => ({ update: currentUpdateConfig }) })) } })) vi.mock('../../src/main/services/logger', () => ({ createLogger: vi.fn(() => ({ info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() })) })) vi.mock('../../src/main/services/update/update-status-publisher', () => ({ publishUpdateStatus: mockPublishUpdateStatus })) vi.mock('../../src/main/services/update/update-storage-client', () => ({ UpdateStorageClient: class { readText = mockReadText downloadToFile = mockDownloadToFile } })) vi.mock('../../src/main/services/update/update-catalog-service', () => ({ UpdateCatalogService: class { loadCatalog = mockLoadCatalog getDialogCatalog = mockGetDialogCatalog resolveUserStatus = mockResolveUserStatus resolveAdminStatus = mockResolveAdminStatus } })) vi.mock('../../src/main/services/update/update-installer', () => ({ UpdateInstaller: class { getDownloadPath = mockGetDownloadPath calculateSha256 = mockCalculateSha256 installDownloadedRelease = mockInstallDownloadedRelease getValidDownloadedRelease = mockGetValidDownloadedRelease } })) describe('UpdateService', () => { beforeEach(() => { vi.clearAllMocks() currentUpdateConfig = { enabled: true, allowDevMode: true, endpoint: 'http://localhost:9000', accessKey: 'key', secretKey: 'secret', bucket: 'bucket', region: 'us-east-1', basePrefix: 'updates/win-portable', checkIntervalMinutes: 30, maxAdminHistoryPerChannel: 10 } mockReadText.mockReset() mockDownloadToFile.mockReset() mockLoadCatalog.mockReset() mockGetDialogCatalog.mockReset() mockResolveUserStatus.mockReset() mockResolveAdminStatus.mockReset() mockGetDownloadPath.mockReset() mockCalculateSha256.mockReset() mockInstallDownloadedRelease.mockReset() mockGetValidDownloadedRelease.mockReset() }) async function loadService() { vi.resetModules() const module = await import('../../src/main/services/update/update-service') ;(module.UpdateService as unknown as { instance: unknown }).instance = null return module.UpdateService.getInstance() } it('initializes enabled update status from config', async () => { const service = await loadService() service.initialize() expect(service.getStatus()).toMatchObject({ enabled: true, supported: true }) }) it('clears update state when user context becomes guest-like', async () => { const service = await loadService() await service.setUserContext(null) expect(service.getStatus()).toMatchObject({ currentUserType: null, phase: 'idle', recommendedRelease: undefined, downloadedRelease: undefined }) expect(mockPublishUpdateStatus).toHaveBeenCalled() }) it('checks updates for user and auto-downloads available recommendation', async () => { const recommended = createRelease('1.1.0') const catalog: UpdateCatalog = { stable: [recommended], preview: [] } const userStatus: Partial = { phase: 'available', recommendedRelease: recommended, latestVersion: recommended.version, latestChannel: recommended.channel, message: `发现稳定版 ${recommended.version}` } mockLoadCatalog.mockResolvedValue(catalog) mockResolveUserStatus.mockResolvedValue(userStatus) mockGetDownloadPath.mockReturnValue('D:/downloads/stable-1.1.0.exe') mockCalculateSha256.mockResolvedValue(recommended.sha256) const service = await loadService() await service.setUserContext('User') expect(mockLoadCatalog).toHaveBeenCalledWith('User') expect(mockResolveUserStatus).toHaveBeenCalled() expect(mockDownloadToFile).toHaveBeenCalledWith( recommended.artifactKey, 'D:/downloads/stable-1.1.0.exe' ) expect(service.getStatus()).toMatchObject({ phase: 'downloaded', latestVersion: '1.1.0', latestChannel: 'stable' }) }) it('returns disabled catalog when update services are unavailable', async () => { currentUpdateConfig = { ...currentUpdateConfig, enabled: false, allowDevMode: false } const service = await loadService() service.initialize() const result = service.getCatalog() expect(result).toEqual({ mode: 'disabled' }) expect(mockGetDialogCatalog).not.toHaveBeenCalled() }) })