d3ro-voice/apps/desktop/tests/main/update-policy-redteam-r3-5.test.ts
Yun Chan ba9ef9741e fix: red-team round 3 hardening across desktop, mobile, core and server
Batch of red-team r3 fixes that were in the working tree before the
2026-09-28 design overhaul, committed as one unit with their tests.

- desktop main: STT timeouts and sidecar, voice recording store, sync
  (credentials, audio, knowledge reindex, push gates), runtime
  provisioner, update policy, AltGr keybindings, voice-command policy,
  dictionary file codec/limits, meeting transcript condensing and a
  local recording ledger so interrupted-session recovery only closes
  meetings this device recorded (a phone's live meeting is left alone).
- mobile: login CSRF via implicit token callbacks rejected, account
  deletion/retention, durable queue retention, knowledge realtime
  without unfiltered DELETE, meeting re-record failure paths, cloud STT
  client, preferences store/resync.
- core: text chunking splits long unbroken transcripts to fit, template
  field policy, dictionary limits, meeting markdown inline handling.
- server: payple webhook policy and cancellation order scope, meeting
  document generation quota, team RPC null-role guard, unified LLM
  quota in-flight accounting, knowledge chunk vector index, meeting
  re-record failure paths (migrations 20260929*).
- ci: portable/runtime feed gates, update-policy schema, Forgejo file
  delete and alias planning.

Four older tests are updated to the new contracts rather than the old
behavior: token-pair auth callbacks are rejected, knowledge realtime no
longer subscribes to DELETE, long transcript lines are split, and
meeting recovery requires the local recording ledger for empty rows.
2026-09-28 20:45:52 +09:00

83 lines
3.1 KiB
TypeScript

// tests/main/update-policy-redteam-r3-5.test.ts
// 원격 정책의 안전 필드(killSwitch, stagingPercentage)가 형식이 틀리면 가장 허용적인 값이
// 아니라 가장 보수적인 값으로 읽히는지 확인한다.
//
// 회귀: `"killSwitch": "true"` 가 false 로, `"stagingPercentage": "5"` / 5.5 가 100% 로
// 조용히 바뀌어 운영자 의도와 반대로 전체 배포되던 문제.
import { describe, it, expect } from 'vitest'
import {
DEFAULT_UPDATE_POLICY,
evaluateUpdateOffer,
parseUpdatePolicy,
} from '../../src/main/update-policy'
const BASE = {
schemaVersion: 1,
defaultChannel: 'latest',
channels: {
latest: { allowPrerelease: false },
beta: { allowPrerelease: true },
alpha: { allowPrerelease: true },
},
minimumSupportedVersion: '1.0.0',
forceInstallBelow: null,
fullInstallOnMajorChange: true,
fullInstallVersionGap: 3,
stagingPercentage: 100,
killSwitch: false,
}
describe('parseUpdatePolicy — 안전 필드 fail-closed', () => {
it.each(['true', 'false', 1, 0, null, {}])('killSwitch=%j 는 켜진 것으로 본다', (value) => {
expect(parseUpdatePolicy({ ...BASE, killSwitch: value }).killSwitch).toBe(true)
})
it.each([5.5, '5', null, Number.NaN, '100'])('stagingPercentage=%j 는 0% 로 본다', (value) => {
expect(parseUpdatePolicy({ ...BASE, stagingPercentage: value }).stagingPercentage).toBe(0)
})
it('형식이 틀린 킬 스위치는 업데이트 제안을 막는다', () => {
const policy = parseUpdatePolicy({ ...BASE, killSwitch: 'true' })
const offer = evaluateUpdateOffer({
policy,
channel: 'latest',
currentVersion: '1.8.0',
targetVersion: '1.9.0',
skippedVersion: null,
deviceId: 'device-a',
})
expect(offer).toMatchObject({ action: 'ignore', reason: 'kill-switch' })
})
it('형식이 틀린 staging 은 필수 업데이트가 아니면 아무에게도 노출하지 않는다', () => {
const policy = parseUpdatePolicy({ ...BASE, stagingPercentage: '5' })
for (const deviceId of ['a', 'b', 'c', 'd', 'e', 'f']) {
const offer = evaluateUpdateOffer({
policy,
channel: 'latest',
currentVersion: '1.8.0',
targetVersion: '1.9.0',
skippedVersion: null,
deviceId,
})
expect(offer).toMatchObject({ action: 'ignore', reason: 'rollout' })
}
})
it('필드가 아예 없으면 기존처럼 기본값을 쓴다', () => {
const { killSwitch: _k, stagingPercentage: _s, ...rest } = BASE
const parsed = parseUpdatePolicy(rest)
expect(parsed.killSwitch).toBe(DEFAULT_UPDATE_POLICY.killSwitch)
expect(parsed.stagingPercentage).toBe(DEFAULT_UPDATE_POLICY.stagingPercentage)
})
it('올바른 값은 그대로(범위 밖 정수는 기존처럼 잘라서) 읽는다', () => {
expect(parseUpdatePolicy({ ...BASE, stagingPercentage: 5, killSwitch: false })).toMatchObject({
stagingPercentage: 5,
killSwitch: false,
})
expect(parseUpdatePolicy({ ...BASE, stagingPercentage: 250 }).stagingPercentage).toBe(100)
expect(parseUpdatePolicy({ ...BASE, stagingPercentage: -3 }).stagingPercentage).toBe(0)
})
})