fix(update): re-evaluate policy on every update transition and skip NSIS updater on portable installs
This commit is contained in:
parent
a4acb42573
commit
88f3dbcd69
11 changed files with 1216 additions and 307 deletions
43
apps/desktop/tests/main/install-kind.test.ts
Normal file
43
apps/desktop/tests/main/install-kind.test.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
// tests/main/install-kind.test.ts
|
||||
// 설치 형태 판별 — 휴대용(Scoop·설치 스크립트·zip) 설치본에서 NSIS 자동 업데이터를 켜지 않는다.
|
||||
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { detectInstallKind, type InstallProbe } from '../../src/main/install-kind'
|
||||
|
||||
function probe(overrides: Partial<InstallProbe> = {}): InstallProbe {
|
||||
return {
|
||||
platform: 'win32',
|
||||
exeDirEntries: ['D3RO Voice.exe', 'resources', 'locales'],
|
||||
hasPortableMarker: false,
|
||||
portableExecutableDir: undefined,
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
describe('detectInstallKind', () => {
|
||||
it('NSIS 제거 프로그램이 있으면 설치자 소유 디렉터리다', () => {
|
||||
expect(detectInstallKind(probe({ exeDirEntries: ['D3RO Voice.exe', 'Uninstall D3RO Voice.exe'] }))).toEqual({
|
||||
kind: 'installer',
|
||||
reason: 'nsis-uninstaller',
|
||||
})
|
||||
})
|
||||
|
||||
it('제거 프로그램이 없으면(Scoop/설치 스크립트/zip) 휴대용이다', () => {
|
||||
expect(detectInstallKind(probe()).kind).toBe('portable')
|
||||
})
|
||||
|
||||
it('휴대용 표식·portable 타깃은 사유만 바꾼다', () => {
|
||||
expect(detectInstallKind(probe({ hasPortableMarker: true })).reason).toBe('portable-marker')
|
||||
expect(detectInstallKind(probe({ portableExecutableDir: 'C:\\apps' })).reason).toBe('portable-target')
|
||||
})
|
||||
|
||||
it('표식이 NSIS 설치본에 섞여도 제거 프로그램이 있으면 자동 업데이트를 끄지 않는다', () => {
|
||||
expect(
|
||||
detectInstallKind(probe({ hasPortableMarker: true, exeDirEntries: ['Uninstall D3RO Voice.exe'] })).kind,
|
||||
).toBe('installer')
|
||||
})
|
||||
|
||||
it('macOS 는 설치 형태와 무관하게 업데이터를 쓴다', () => {
|
||||
expect(detectInstallKind(probe({ platform: 'darwin' })).kind).toBe('installer')
|
||||
})
|
||||
})
|
||||
|
|
@ -57,6 +57,7 @@ function harness(opts: { answers?: ConsentAnswer[]; policies?: UpdatePolicy[] }
|
|||
quitAndInstall: vi.fn(),
|
||||
setChannel: vi.fn(),
|
||||
setDifferentialDisabled: vi.fn(),
|
||||
setAutoInstallOnQuit: vi.fn(),
|
||||
}
|
||||
const policySource: PolicySource & { loads: number } = {
|
||||
loads: 0,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,349 @@
|
|||
// tests/main/services/update-service-redteam-r2-11.test.ts
|
||||
// 자동 업데이트 회귀 테스트 (라운드 2, 가짜 포트 주입):
|
||||
// 1) 킬 스위치·rollout 중단이 받는 중/받아 둔 설치 파일의 '종료 시 설치'까지 막는다
|
||||
// 2) 열린 동의 대화상자의 동의가 electron-updater 가 나중에 본(게이팅된) 버전에 묶이지 않는다
|
||||
// 3) 강제·필수 업데이트는 재시작 단계에서 미룰 수 없다
|
||||
//
|
||||
// 가짜 업데이터는 electron-updater 의 관련 동작만 흉내 낸다:
|
||||
// - update-available 직전에 대상 버전을 교체하고, downloadUpdate() 는 그 대상을 받는다
|
||||
// - 다운로드가 끝나면 autoInstallOnAppQuit 가 켜져 있을 때만 종료 핸들러를 건다
|
||||
// - 종료 시 autoInstallOnAppQuit 가 여전히 켜져 있을 때만 설치한다
|
||||
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||
import { DEFAULT_UPDATE_POLICY, rolloutBucket, type UpdatePolicy } from '../../../src/main/update-policy'
|
||||
import type {
|
||||
ConsentAnswer,
|
||||
PolicySource,
|
||||
RestartAnswer,
|
||||
UpdatePrompter,
|
||||
UpdaterEventHandlers,
|
||||
UpdaterPort,
|
||||
} from '../../../src/main/services/UpdateService'
|
||||
|
||||
vi.mock('../../../src/main/services/LoggerService', () => ({
|
||||
getLogger: () => ({ info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() }),
|
||||
}))
|
||||
|
||||
const store = vi.hoisted(() => ({ values: {} as Record<string, unknown> }))
|
||||
vi.mock('../../../src/main/services/ConfigService', () => ({
|
||||
configGet: vi.fn((key: string) => store.values[key]),
|
||||
configSet: vi.fn((key: string, value: unknown) => {
|
||||
store.values[key] = value
|
||||
}),
|
||||
configGetAll: vi.fn(() => store.values),
|
||||
}))
|
||||
|
||||
type ServiceModule = typeof import('../../../src/main/services/UpdateService')
|
||||
let UpdateService: ServiceModule['UpdateService']
|
||||
|
||||
function policy(overrides: Partial<UpdatePolicy> = {}): UpdatePolicy {
|
||||
return { ...DEFAULT_UPDATE_POLICY, channels: { ...DEFAULT_UPDATE_POLICY.channels }, ...overrides }
|
||||
}
|
||||
|
||||
async function flush(): Promise<void> {
|
||||
for (let i = 0; i < 10; i++) await new Promise((resolve) => setImmediate(resolve))
|
||||
}
|
||||
|
||||
interface Deferred<T> {
|
||||
promise: Promise<T>
|
||||
resolve(value: T): void
|
||||
}
|
||||
|
||||
function deferred<T>(): Deferred<T> {
|
||||
let resolve!: (value: T) => void
|
||||
const promise = new Promise<T>((r) => {
|
||||
resolve = r
|
||||
})
|
||||
return { promise, resolve }
|
||||
}
|
||||
|
||||
function harness(opts: {
|
||||
policy?: UpdatePolicy
|
||||
currentVersion?: string
|
||||
consent?: () => Promise<ConsentAnswer>
|
||||
restart?: () => Promise<RestartAnswer>
|
||||
}) {
|
||||
let currentPolicy = opts.policy ?? policy()
|
||||
let handlers: UpdaterEventHandlers | null = null
|
||||
|
||||
const fake = {
|
||||
/** electron-updater 의 updateInfoAndProvider 에 해당 */
|
||||
target: null as string | null,
|
||||
downloading: null as string | null,
|
||||
downloaded: null as string | null,
|
||||
autoInstallOnAppQuit: true,
|
||||
quitHandlerAdded: false,
|
||||
downloads: [] as string[],
|
||||
checks: 0,
|
||||
installs: [] as string[],
|
||||
}
|
||||
|
||||
const updater: UpdaterPort = {
|
||||
checkForUpdates: vi.fn(async () => {
|
||||
fake.checks++
|
||||
}),
|
||||
downloadUpdate: vi.fn(async () => {
|
||||
if (fake.target) {
|
||||
fake.downloads.push(fake.target)
|
||||
fake.downloading = fake.target
|
||||
}
|
||||
}),
|
||||
quitAndInstall: vi.fn(() => {
|
||||
if (fake.downloaded) fake.installs.push(fake.downloaded)
|
||||
}),
|
||||
setChannel: vi.fn(),
|
||||
setDifferentialDisabled: vi.fn(),
|
||||
setAutoInstallOnQuit: vi.fn((enabled: boolean) => {
|
||||
fake.autoInstallOnAppQuit = enabled
|
||||
}),
|
||||
}
|
||||
|
||||
const policySource: PolicySource = { load: vi.fn(async () => currentPolicy) }
|
||||
const prompter: UpdatePrompter = {
|
||||
askConsent: vi.fn(opts.consent ?? (async () => 'download' as const)),
|
||||
askRestart: vi.fn(opts.restart ?? (async () => 'later' as const)),
|
||||
}
|
||||
|
||||
const service = new UpdateService({
|
||||
createUpdater: (h) => {
|
||||
handlers = h
|
||||
return updater
|
||||
},
|
||||
policySource,
|
||||
prompter,
|
||||
currentVersion: () => opts.currentVersion ?? '1.9.0',
|
||||
now: () => 1_000_000,
|
||||
})
|
||||
service.initialize()
|
||||
service.dispose()
|
||||
|
||||
return {
|
||||
service,
|
||||
updater,
|
||||
prompter,
|
||||
fake,
|
||||
setPolicy(next: UpdatePolicy) {
|
||||
currentPolicy = next
|
||||
},
|
||||
/** electron-updater 가 새 버전을 발견: 대상 교체 → update-available */
|
||||
offer(version: string) {
|
||||
fake.target = version
|
||||
;(handlers as UpdaterEventHandlers).onAvailable({ version })
|
||||
},
|
||||
/** 다운로드 완료: update-downloaded → (동기로) 종료 핸들러 등록 */
|
||||
finishDownload() {
|
||||
const version = fake.downloading as string
|
||||
fake.downloading = null
|
||||
fake.downloaded = version
|
||||
;(handlers as UpdaterEventHandlers).onDownloaded(version)
|
||||
if (fake.autoInstallOnAppQuit) fake.quitHandlerAdded = true
|
||||
},
|
||||
/** 앱 종료: electron-updater 종료 핸들러 흉내 */
|
||||
quitApp() {
|
||||
if (fake.quitHandlerAdded && fake.autoInstallOnAppQuit && fake.downloaded) {
|
||||
fake.installs.push(fake.downloaded)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
store.values = { updateDeviceId: 'device-1' }
|
||||
UpdateService = (await import('../../../src/main/services/UpdateService')).UpdateService
|
||||
})
|
||||
|
||||
describe('1) 킬 스위치·rollout 중단은 받아 둔 설치 파일에도 적용된다', () => {
|
||||
it("'종료 시 설치'를 고른 뒤 킬 스위치가 켜지면 종료해도 설치하지 않는다", async () => {
|
||||
const h = harness({})
|
||||
await h.service.checkForUpdates()
|
||||
h.offer('1.9.1')
|
||||
await flush()
|
||||
expect(h.fake.downloads).toEqual(['1.9.1'])
|
||||
h.finishDownload()
|
||||
await flush()
|
||||
expect(h.prompter.askRestart).toHaveBeenCalledTimes(1)
|
||||
|
||||
h.setPolicy(policy({ killSwitch: true }))
|
||||
await h.service.checkForUpdates()
|
||||
expect(h.fake.autoInstallOnAppQuit).toBe(false)
|
||||
|
||||
h.quitApp()
|
||||
expect(h.fake.installs).toEqual([])
|
||||
})
|
||||
|
||||
it('stagingPercentage=0 으로 rollout 을 멈춰도 받아 둔 설치 파일은 설치하지 않는다', async () => {
|
||||
const h = harness({})
|
||||
await h.service.checkForUpdates()
|
||||
h.offer('1.9.1')
|
||||
await flush()
|
||||
h.finishDownload()
|
||||
await flush()
|
||||
|
||||
h.setPolicy(policy({ stagingPercentage: 0 }))
|
||||
await h.service.checkForUpdates()
|
||||
h.quitApp()
|
||||
expect(h.fake.installs).toEqual([])
|
||||
})
|
||||
|
||||
it('받는 중에 킬 스위치가 켜지면 다운로드가 끝나도 재시작을 묻지 않고 종료 핸들러도 걸리지 않는다', async () => {
|
||||
const h = harness({})
|
||||
await h.service.checkForUpdates()
|
||||
h.offer('1.9.1')
|
||||
await flush()
|
||||
expect(h.fake.downloading).toBe('1.9.1')
|
||||
|
||||
h.setPolicy(policy({ killSwitch: true }))
|
||||
await h.service.checkForUpdates()
|
||||
expect(h.fake.autoInstallOnAppQuit).toBe(false)
|
||||
|
||||
h.finishDownload()
|
||||
await flush()
|
||||
expect(h.prompter.askRestart).not.toHaveBeenCalled()
|
||||
h.quitApp()
|
||||
expect(h.fake.installs).toEqual([])
|
||||
})
|
||||
|
||||
it('킬 스위치가 풀리면 종료 시 설치를 되살린다', async () => {
|
||||
const h = harness({})
|
||||
await h.service.checkForUpdates()
|
||||
h.offer('1.9.1')
|
||||
await flush()
|
||||
h.finishDownload()
|
||||
await flush()
|
||||
|
||||
h.setPolicy(policy({ killSwitch: true }))
|
||||
await h.service.checkForUpdates()
|
||||
expect(h.fake.autoInstallOnAppQuit).toBe(false)
|
||||
|
||||
h.setPolicy(policy())
|
||||
await h.service.checkForUpdates()
|
||||
expect(h.fake.autoInstallOnAppQuit).toBe(true)
|
||||
h.quitApp()
|
||||
expect(h.fake.installs).toEqual(['1.9.1'])
|
||||
})
|
||||
|
||||
it("재시작 대화상자가 떠 있는 동안 킬 스위치가 켜지면 '지금 재시작'이어도 설치하지 않는다", async () => {
|
||||
const answer = deferred<RestartAnswer>()
|
||||
const h = harness({ restart: () => answer.promise })
|
||||
await h.service.checkForUpdates()
|
||||
h.offer('1.9.1')
|
||||
await flush()
|
||||
h.finishDownload()
|
||||
await flush()
|
||||
|
||||
h.setPolicy(policy({ killSwitch: true }))
|
||||
answer.resolve('restart')
|
||||
await flush()
|
||||
expect(h.updater.quitAndInstall).not.toHaveBeenCalled()
|
||||
expect(h.fake.autoInstallOnAppQuit).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('2) 동의는 동의한 버전에만 묶인다', () => {
|
||||
it('동의 대화상자가 열려 있는 동안에는 업데이터 체크를 건너뛴다(정책은 갱신)', async () => {
|
||||
const consent = deferred<ConsentAnswer>()
|
||||
const h = harness({ consent: () => consent.promise })
|
||||
await h.service.checkForUpdates()
|
||||
h.offer('1.9.1')
|
||||
await flush()
|
||||
expect(h.prompter.askConsent).toHaveBeenCalledTimes(1)
|
||||
|
||||
const checksBefore = vi.mocked(h.updater.checkForUpdates).mock.calls.length
|
||||
await h.service.checkForUpdates()
|
||||
expect(vi.mocked(h.updater.checkForUpdates).mock.calls.length).toBe(checksBefore)
|
||||
|
||||
consent.resolve('download')
|
||||
await flush()
|
||||
expect(h.fake.downloads).toEqual(['1.9.1'])
|
||||
})
|
||||
|
||||
it('대화상자가 열린 사이 rollout 밖의 1.9.2 가 대상이 되면, 1.9.1 동의로 1.9.2 를 받지 않는다', async () => {
|
||||
// 1.9.1 은 rollout 안, 1.9.2 는 rollout 밖인 기기를 고른다
|
||||
let deviceId = ''
|
||||
for (let i = 0; i < 1000 && !deviceId; i++) {
|
||||
const candidate = `device-${i}`
|
||||
if (rolloutBucket(candidate, '1.9.1') < 50 && rolloutBucket(candidate, '1.9.2') >= 50) deviceId = candidate
|
||||
}
|
||||
expect(deviceId).not.toBe('')
|
||||
store.values.updateDeviceId = deviceId
|
||||
|
||||
const consent = deferred<ConsentAnswer>()
|
||||
const h = harness({ policy: policy({ stagingPercentage: 50 }), consent: () => consent.promise })
|
||||
await h.service.checkForUpdates()
|
||||
h.offer('1.9.1')
|
||||
await flush()
|
||||
|
||||
// 누군가(수동 체크 등) 업데이터를 다시 돌려 1.9.2 가 대상이 됐다 — 게이팅은 1.9.2 를 무시
|
||||
h.offer('1.9.2')
|
||||
await flush()
|
||||
expect(h.prompter.askConsent).toHaveBeenCalledTimes(1)
|
||||
|
||||
consent.resolve('download')
|
||||
await flush()
|
||||
expect(h.updater.downloadUpdate).not.toHaveBeenCalled()
|
||||
expect(h.fake.downloads).toEqual([])
|
||||
})
|
||||
|
||||
it('대화상자가 열린 사이 킬 스위치가 켜지면 동의해도 받지 않는다', async () => {
|
||||
const consent = deferred<ConsentAnswer>()
|
||||
const h = harness({ consent: () => consent.promise })
|
||||
await h.service.checkForUpdates()
|
||||
h.offer('1.9.1')
|
||||
await flush()
|
||||
|
||||
h.setPolicy(policy({ killSwitch: true }))
|
||||
await h.service.checkForUpdates()
|
||||
consent.resolve('download')
|
||||
await flush()
|
||||
expect(h.fake.downloads).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('3) 강제·필수 업데이트는 재시작 단계에서 미룰 수 없다', () => {
|
||||
it('forceInstallBelow 미만이면 동의 없이 받고, 재시작 대화상자에서 미뤄도 설치한다', async () => {
|
||||
const h = harness({ policy: policy({ forceInstallBelow: '1.9.1' }), restart: async () => 'later' })
|
||||
await h.service.checkForUpdates()
|
||||
h.offer('1.9.1')
|
||||
await flush()
|
||||
expect(h.prompter.askConsent).not.toHaveBeenCalled()
|
||||
expect(h.fake.downloads).toEqual(['1.9.1'])
|
||||
|
||||
h.finishDownload()
|
||||
await flush()
|
||||
expect(h.prompter.askRestart).toHaveBeenCalledWith('1.9.1', { mandatory: true })
|
||||
expect(h.updater.quitAndInstall).toHaveBeenCalledWith(false, true)
|
||||
expect(h.fake.installs).toEqual(['1.9.1'])
|
||||
})
|
||||
|
||||
it('최소 지원 버전 미만(필수)도 재시작을 미룰 수 없다', async () => {
|
||||
const h = harness({ policy: policy({ minimumSupportedVersion: '1.9.1' }), restart: async () => 'later' })
|
||||
await h.service.checkForUpdates()
|
||||
h.offer('1.9.1')
|
||||
await flush()
|
||||
h.finishDownload()
|
||||
await flush()
|
||||
expect(h.prompter.askRestart).toHaveBeenCalledWith('1.9.1', { mandatory: true })
|
||||
expect(h.updater.quitAndInstall).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("일반 업데이트는 '종료 시 설치'를 고르면 지금 설치하지 않고, '지금 재시작'이면 설치한다", async () => {
|
||||
const later = harness({ restart: async () => 'later' })
|
||||
await later.service.checkForUpdates()
|
||||
later.offer('1.9.1')
|
||||
await flush()
|
||||
later.finishDownload()
|
||||
await flush()
|
||||
expect(later.prompter.askRestart).toHaveBeenCalledWith('1.9.1', { mandatory: false })
|
||||
expect(later.updater.quitAndInstall).not.toHaveBeenCalled()
|
||||
expect(later.fake.autoInstallOnAppQuit).toBe(true)
|
||||
|
||||
const now = harness({ restart: async () => 'restart' })
|
||||
await now.service.checkForUpdates()
|
||||
now.offer('1.9.1')
|
||||
await flush()
|
||||
now.finishDownload()
|
||||
await flush()
|
||||
expect(now.updater.quitAndInstall).toHaveBeenCalledWith(false, true)
|
||||
})
|
||||
})
|
||||
96
apps/desktop/tests/main/update-session.test.ts
Normal file
96
apps/desktop/tests/main/update-session.test.ts
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
// tests/main/update-session.test.ts
|
||||
// 업데이트 세션 상태 머신 (순수) — 전이마다 정책을 다시 평가하는지 표로 검증한다.
|
||||
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { DEFAULT_UPDATE_POLICY, type UpdatePolicy } from '../../src/main/update-policy'
|
||||
import {
|
||||
IDLE_UPDATE_SESSION,
|
||||
nextUpdateAction,
|
||||
reduceUpdateSession,
|
||||
type UpdateGateContext,
|
||||
type UpdateSession,
|
||||
} from '../../src/main/update-session'
|
||||
|
||||
function ctx(overrides: Partial<UpdatePolicy> = {}): UpdateGateContext {
|
||||
return {
|
||||
policy: { ...DEFAULT_UPDATE_POLICY, channels: { ...DEFAULT_UPDATE_POLICY.channels }, ...overrides },
|
||||
channel: 'latest',
|
||||
currentVersion: '1.9.0',
|
||||
deviceId: 'device-1',
|
||||
}
|
||||
}
|
||||
|
||||
const downloading: UpdateSession = { consenting: null, payload: { version: '1.9.1', phase: 'downloading' } }
|
||||
const downloaded: UpdateSession = { consenting: null, payload: { version: '1.9.1', phase: 'downloaded' } }
|
||||
|
||||
describe('reduceUpdateSession', () => {
|
||||
it('동의 대화상자와 설치 파일 슬롯을 따로 추적한다', () => {
|
||||
let s = reduceUpdateSession(downloaded, { type: 'consent-opened', version: '1.9.2' })
|
||||
expect(s).toEqual({ consenting: '1.9.2', payload: { version: '1.9.1', phase: 'downloaded' } })
|
||||
s = reduceUpdateSession(s, { type: 'consent-closed' })
|
||||
s = reduceUpdateSession(s, { type: 'download-started', version: '1.9.2' })
|
||||
expect(s.payload).toEqual({ version: '1.9.2', phase: 'downloading' })
|
||||
s = reduceUpdateSession(s, { type: 'download-finished', version: '1.9.2' })
|
||||
expect(s.payload).toEqual({ version: '1.9.2', phase: 'downloaded' })
|
||||
})
|
||||
|
||||
it('실패는 받는 중인 설치 파일만 비운다 (체크 실패가 받아 둔 파일을 지우지 않는다)', () => {
|
||||
expect(reduceUpdateSession(downloading, { type: 'download-failed' }).payload).toBeNull()
|
||||
expect(reduceUpdateSession(downloaded, { type: 'download-failed' })).toBe(downloaded)
|
||||
expect(reduceUpdateSession(IDLE_UPDATE_SESSION, { type: 'download-failed' })).toBe(IDLE_UPDATE_SESSION)
|
||||
})
|
||||
})
|
||||
|
||||
describe('nextUpdateAction', () => {
|
||||
it.each([
|
||||
// [설명, 세션, 트리거, 정책, 기대 동작]
|
||||
['동의 → 허용', IDLE_UPDATE_SESSION, { type: 'consented', version: '1.9.1', offeredVersion: '1.9.1' }, {}, 'download'],
|
||||
['동의 → 대상 바뀜', IDLE_UPDATE_SESSION, { type: 'consented', version: '1.9.1', offeredVersion: '1.9.2' }, {}, 'drop-consent'],
|
||||
['동의 → 킬 스위치', IDLE_UPDATE_SESSION, { type: 'consented', version: '1.9.1', offeredVersion: '1.9.1' }, { killSwitch: true }, 'drop-consent'],
|
||||
['동의 → rollout 중단', IDLE_UPDATE_SESSION, { type: 'consented', version: '1.9.1', offeredVersion: '1.9.1' }, { stagingPercentage: 0 }, 'drop-consent'],
|
||||
['설치 파일 없음 → 정책 갱신', IDLE_UPDATE_SESSION, { type: 'policy-refreshed' }, { killSwitch: true }, 'none'],
|
||||
['받는 중 → 킬 스위치', downloading, { type: 'policy-refreshed' }, { killSwitch: true }, 'cancel-pending'],
|
||||
['받는 중 → 허용 유지', downloading, { type: 'policy-refreshed' }, {}, 'keep-pending'],
|
||||
['받아 둠 → 킬 스위치', downloaded, { type: 'policy-refreshed' }, { killSwitch: true }, 'cancel-pending'],
|
||||
['받아 둠 → rollout 중단', downloaded, { type: 'policy-refreshed' }, { stagingPercentage: 0 }, 'cancel-pending'],
|
||||
['받아 둠 → 허용 유지', downloaded, { type: 'policy-refreshed' }, {}, 'keep-pending'],
|
||||
['완료 → 재시작 묻기', downloaded, { type: 'download-finished' }, {}, 'ask-restart'],
|
||||
['완료 → 킬 스위치', downloaded, { type: 'download-finished' }, { killSwitch: true }, 'cancel-pending'],
|
||||
['재시작 수락', downloaded, { type: 'restart-answered', answer: 'restart' }, {}, 'install'],
|
||||
['재시작 미룸', downloaded, { type: 'restart-answered', answer: 'later' }, {}, 'keep-pending'],
|
||||
['재시작 미룸 (강제 설치 하한)', downloaded, { type: 'restart-answered', answer: 'later' }, { forceInstallBelow: '1.9.1' }, 'install'],
|
||||
['재시작 미룸 (최소 지원)', downloaded, { type: 'restart-answered', answer: 'later' }, { minimumSupportedVersion: '1.9.1' }, 'install'],
|
||||
['재시작 수락 → 그 사이 킬 스위치', downloaded, { type: 'restart-answered', answer: 'restart' }, { killSwitch: true }, 'cancel-pending'],
|
||||
] as const)('%s', (_label, session, trigger, overrides, expected) => {
|
||||
expect(nextUpdateAction(session, trigger, ctx(overrides)).type).toBe(expected)
|
||||
})
|
||||
|
||||
it('완료 시 필수 여부를 재시작 대화상자에 넘긴다', () => {
|
||||
expect(nextUpdateAction(downloaded, { type: 'download-finished' }, ctx({ forceInstallBelow: '1.9.1' }))).toEqual({
|
||||
type: 'ask-restart',
|
||||
version: '1.9.1',
|
||||
mandatory: true,
|
||||
})
|
||||
expect(nextUpdateAction(downloaded, { type: 'download-finished' }, ctx())).toEqual({
|
||||
type: 'ask-restart',
|
||||
version: '1.9.1',
|
||||
mandatory: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('필수 업데이트는 rollout 중단(stagingPercentage=0)에도 설치 파일을 유지한다', () => {
|
||||
expect(
|
||||
nextUpdateAction(downloaded, { type: 'policy-refreshed' }, ctx({ stagingPercentage: 0, minimumSupportedVersion: '1.9.1' }))
|
||||
.type,
|
||||
).toBe('keep-pending')
|
||||
})
|
||||
|
||||
it('다운로드 결정은 동의 시점의 정책으로 다시 계산한다 (전체/차분)', () => {
|
||||
const action = nextUpdateAction(
|
||||
IDLE_UPDATE_SESSION,
|
||||
{ type: 'consented', version: '2.0.0', offeredVersion: '2.0.0' },
|
||||
ctx(),
|
||||
)
|
||||
expect(action).toMatchObject({ type: 'download', version: '2.0.0', decision: { forceFull: true } })
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue