refactor(desktop): session-scoped voice runtime, STT/LLM ports, caption ownership and meeting export fixes
This commit is contained in:
parent
0adedf5e7b
commit
83cf9133ac
39 changed files with 4787 additions and 1061 deletions
|
|
@ -0,0 +1,232 @@
|
|||
// tests/main/services/update-service-redteam-r1-3.test.ts
|
||||
// 자동 업데이트 회귀 테스트 (가짜 포트 주입):
|
||||
// - 원격 정책을 체크마다 다시 받는다 (킬 스위치·rollout 이 실행 중 클라이언트에 반영)
|
||||
// - '나중에' 이후에도 새 버전·필수 격상·간격 경과 시 다시 묻는다
|
||||
// - evaluateUpdateOffer 게이팅 순서
|
||||
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||
import {
|
||||
DEFAULT_UPDATE_POLICY,
|
||||
evaluateUpdateOffer,
|
||||
shouldPromptForUpdate,
|
||||
UPDATE_REPROMPT_INTERVAL_MS,
|
||||
type UpdatePolicy,
|
||||
} from '../../../src/main/update-policy'
|
||||
import type {
|
||||
ConsentAnswer,
|
||||
PolicySource,
|
||||
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 }
|
||||
}
|
||||
|
||||
function harness(opts: { answers?: ConsentAnswer[]; policies?: UpdatePolicy[] } = {}) {
|
||||
const answers = [...(opts.answers ?? [])]
|
||||
const policies = [...(opts.policies ?? [policy()])]
|
||||
let clock = 1_000_000
|
||||
let handlers: UpdaterEventHandlers | null = null
|
||||
const updater: UpdaterPort & { checks: number; downloads: number } = {
|
||||
checks: 0,
|
||||
downloads: 0,
|
||||
checkForUpdates: vi.fn(async () => {
|
||||
updater.checks++
|
||||
}),
|
||||
downloadUpdate: vi.fn(async () => {
|
||||
updater.downloads++
|
||||
}),
|
||||
quitAndInstall: vi.fn(),
|
||||
setChannel: vi.fn(),
|
||||
setDifferentialDisabled: vi.fn(),
|
||||
}
|
||||
const policySource: PolicySource & { loads: number } = {
|
||||
loads: 0,
|
||||
load: vi.fn(async () => {
|
||||
policySource.loads++
|
||||
return policies.length > 1 ? (policies.shift() as UpdatePolicy) : policies[0]
|
||||
}),
|
||||
}
|
||||
const consents: string[] = []
|
||||
const prompter: UpdatePrompter = {
|
||||
askConsent: vi.fn(async ({ version }) => {
|
||||
consents.push(version)
|
||||
return answers.shift() ?? 'later'
|
||||
}),
|
||||
askRestart: vi.fn(async () => 'later' as const),
|
||||
}
|
||||
const service = new UpdateService({
|
||||
createUpdater: (h) => {
|
||||
handlers = h
|
||||
return updater
|
||||
},
|
||||
policySource,
|
||||
prompter,
|
||||
currentVersion: () => '1.9.0',
|
||||
now: () => clock,
|
||||
})
|
||||
service.initialize()
|
||||
service.dispose() // 타이머는 쓰지 않는다 — 테스트가 직접 호출
|
||||
return {
|
||||
service,
|
||||
updater,
|
||||
policySource,
|
||||
consents,
|
||||
handlers: () => handlers as UpdaterEventHandlers,
|
||||
advance: (ms: number) => {
|
||||
clock += ms
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
store.values = { updateDeviceId: 'device-1' }
|
||||
UpdateService = (await import('../../../src/main/services/UpdateService')).UpdateService
|
||||
})
|
||||
|
||||
describe('UpdateService — 원격 정책 갱신', () => {
|
||||
it('체크할 때마다 정책을 다시 받고, 킬 스위치가 켜지면 확인을 멈춘다', async () => {
|
||||
const h = harness({ policies: [policy(), policy({ killSwitch: true })] })
|
||||
|
||||
await h.service.checkForUpdates()
|
||||
expect(h.updater.checks).toBe(1)
|
||||
|
||||
await h.service.checkForUpdates()
|
||||
expect(h.policySource.loads).toBe(2)
|
||||
expect(h.updater.checks).toBe(1)
|
||||
})
|
||||
|
||||
it('킬 스위치가 켜진 뒤 도착한 update-available 도 무시한다', async () => {
|
||||
const h = harness({ policies: [policy({ killSwitch: true })] })
|
||||
await h.service.checkForUpdates()
|
||||
await h.service.handleUpdateAvailable({ version: '1.9.1' })
|
||||
expect(h.consents).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('UpdateService — 다시 묻기', () => {
|
||||
it("'나중에' 이후 새 버전이 나오면 다시 묻는다", async () => {
|
||||
const h = harness({ answers: ['later', 'later'] })
|
||||
await h.service.checkForUpdates()
|
||||
await h.service.handleUpdateAvailable({ version: '1.9.1' })
|
||||
await h.service.handleUpdateAvailable({ version: '1.9.2' })
|
||||
expect(h.consents).toEqual(['1.9.1', '1.9.2'])
|
||||
})
|
||||
|
||||
it('같은 버전은 간격 안에서는 다시 묻지 않고, 간격이 지나면 다시 묻는다', async () => {
|
||||
const h = harness({ answers: ['later', 'later'] })
|
||||
await h.service.checkForUpdates()
|
||||
await h.service.handleUpdateAvailable({ version: '1.9.1' })
|
||||
await h.service.handleUpdateAvailable({ version: '1.9.1' })
|
||||
expect(h.consents).toEqual(['1.9.1'])
|
||||
|
||||
h.advance(UPDATE_REPROMPT_INTERVAL_MS)
|
||||
await h.service.handleUpdateAvailable({ version: '1.9.1' })
|
||||
expect(h.consents).toEqual(['1.9.1', '1.9.1'])
|
||||
})
|
||||
|
||||
it('필수로 격상되면 간격과 무관하게 다시 묻는다', async () => {
|
||||
const h = harness({
|
||||
answers: ['later', 'download'],
|
||||
policies: [policy(), policy({ minimumSupportedVersion: '1.9.1' })],
|
||||
})
|
||||
await h.service.checkForUpdates()
|
||||
await h.service.handleUpdateAvailable({ version: '1.9.1' })
|
||||
await h.service.checkForUpdates()
|
||||
await h.service.handleUpdateAvailable({ version: '1.9.1' })
|
||||
expect(h.consents).toEqual(['1.9.1', '1.9.1'])
|
||||
expect(h.updater.downloads).toBe(1)
|
||||
})
|
||||
|
||||
it('수락한 다운로드가 실패하면 다음 체크에서 다시 묻는다', async () => {
|
||||
const h = harness({ answers: ['download', 'later'] })
|
||||
await h.service.checkForUpdates()
|
||||
await h.service.handleUpdateAvailable({ version: '1.9.1' })
|
||||
expect(h.updater.downloads).toBe(1)
|
||||
|
||||
h.handlers().onError('network down')
|
||||
await h.service.handleUpdateAvailable({ version: '1.9.1' })
|
||||
expect(h.consents).toEqual(['1.9.1', '1.9.1'])
|
||||
})
|
||||
|
||||
it("'이 버전 건너뛰기' 는 그 버전을 다시 묻지 않는다", async () => {
|
||||
const h = harness({ answers: ['skip'] })
|
||||
await h.service.checkForUpdates()
|
||||
await h.service.handleUpdateAvailable({ version: '1.9.1' })
|
||||
h.advance(UPDATE_REPROMPT_INTERVAL_MS * 2)
|
||||
await h.service.handleUpdateAvailable({ version: '1.9.1' })
|
||||
expect(h.consents).toEqual(['1.9.1'])
|
||||
expect(store.values.skippedUpdateVersion).toBe('1.9.1')
|
||||
})
|
||||
})
|
||||
|
||||
describe('evaluateUpdateOffer 게이팅 순서', () => {
|
||||
const base = {
|
||||
channel: 'latest' as const,
|
||||
currentVersion: '1.9.0',
|
||||
skippedVersion: null,
|
||||
deviceId: 'device-1',
|
||||
}
|
||||
|
||||
it('킬 스위치가 가장 먼저다', () => {
|
||||
const offer = evaluateUpdateOffer({ ...base, policy: policy({ killSwitch: true }), targetVersion: '1.9.1' })
|
||||
expect(offer).toMatchObject({ action: 'ignore', reason: 'kill-switch' })
|
||||
})
|
||||
|
||||
it('stable 채널은 prerelease 를 거부한다', () => {
|
||||
const offer = evaluateUpdateOffer({ ...base, policy: policy(), targetVersion: '1.10.0-beta.1' })
|
||||
expect(offer).toMatchObject({ action: 'ignore', reason: 'channel' })
|
||||
})
|
||||
|
||||
it('필수 업데이트는 건너뛰기·rollout 을 무시한다', () => {
|
||||
const offer = evaluateUpdateOffer({
|
||||
...base,
|
||||
policy: policy({ minimumSupportedVersion: '1.9.1', stagingPercentage: 0 }),
|
||||
targetVersion: '1.9.1',
|
||||
skippedVersion: '1.9.1',
|
||||
})
|
||||
expect(offer).toMatchObject({ action: 'prompt', decision: { mandatory: true } })
|
||||
})
|
||||
|
||||
it('건너뛴 버전 / rollout 밖은 무시한다', () => {
|
||||
expect(
|
||||
evaluateUpdateOffer({ ...base, policy: policy(), targetVersion: '1.9.1', skippedVersion: '1.9.1' }),
|
||||
).toMatchObject({ action: 'ignore', reason: 'skipped' })
|
||||
expect(
|
||||
evaluateUpdateOffer({ ...base, policy: policy({ stagingPercentage: 0 }), targetVersion: '1.9.1' }),
|
||||
).toMatchObject({ action: 'ignore', reason: 'rollout' })
|
||||
})
|
||||
|
||||
it('강제 설치 하한 미만이면 대화상자 없이 다운로드한다', () => {
|
||||
const offer = evaluateUpdateOffer({ ...base, policy: policy({ forceInstallBelow: '1.9.1' }), targetVersion: '2.0.0' })
|
||||
expect(offer).toMatchObject({ action: 'force-download', isMajorUpgrade: true })
|
||||
})
|
||||
|
||||
it('shouldPromptForUpdate: 기록 없음 / 새 버전 / 필수 격상 / 간격 경과', () => {
|
||||
const last = { version: '1.9.1', mandatory: false, at: 0 }
|
||||
expect(shouldPromptForUpdate(null, { version: '1.9.1', mandatory: false }, 0)).toBe(true)
|
||||
expect(shouldPromptForUpdate(last, { version: '1.9.2', mandatory: false }, 1)).toBe(true)
|
||||
expect(shouldPromptForUpdate(last, { version: '1.9.1', mandatory: true }, 1)).toBe(true)
|
||||
expect(shouldPromptForUpdate(last, { version: '1.9.1', mandatory: false }, 1)).toBe(false)
|
||||
expect(shouldPromptForUpdate(last, { version: '1.9.1', mandatory: false }, UPDATE_REPROMPT_INTERVAL_MS)).toBe(true)
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue