diff --git a/apps/desktop/src/renderer/components/keybinding/Keycap.tsx b/apps/desktop/src/renderer/components/keybinding/Keycap.tsx index ff6df97..a2c4cfd 100644 --- a/apps/desktop/src/renderer/components/keybinding/Keycap.tsx +++ b/apps/desktop/src/renderer/components/keybinding/Keycap.tsx @@ -28,7 +28,7 @@ interface KeycapProps { } /** 모노 폰트는 라틴 키 이름에만 — 한글("마우스 왼쪽 버튼")은 모노에 글리프가 없어 자간이 벌어진다. */ -const NON_LATIN = /[^\u0000-\u024f]/ +const NON_LATIN = /[^ -\u024f]/ export function Keycap({ children, size = 'md', muted = false }: KeycapProps): React.ReactElement { const spec = SIZE_SPEC[size] diff --git a/apps/desktop/tests/main/services/voice-recording-store-redteam-r3-1.test.ts b/apps/desktop/tests/main/services/voice-recording-store-redteam-r3-1.test.ts index 870b783..c974202 100644 --- a/apps/desktop/tests/main/services/voice-recording-store-redteam-r3-1.test.ts +++ b/apps/desktop/tests/main/services/voice-recording-store-redteam-r3-1.test.ts @@ -17,19 +17,6 @@ vi.mock('../../../src/main/services/LoggerService', () => ({ getLogger: () => ({ info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() }), })) -interface Deferred { - promise: Promise - resolve: (value: T) => void -} - -function deferred(): Deferred { - let resolve!: (value: T) => void - const promise = new Promise((res) => { - resolve = res - }) - return { promise, resolve } -} - type SttResult = { text: string; segments: never[]; language: string; duration: number; processingTime: number } const result = (text: string): SttResult => ({ text, segments: [], language: 'ko', duration: 1, processingTime: 1 }) @@ -126,7 +113,6 @@ vi.mock('../../../src/main/services/ScreenContextService', () => ({ })) type VoiceModeModule = typeof import('../../../src/main/services/VoiceModeService') -let getVoiceModeService: VoiceModeModule['getVoiceModeService'] let createWithStore: VoiceModeModule['createVoiceModeServiceForTests'] const SPEECH = Buffer.alloc(16000 * 2) // 1초 @@ -140,21 +126,6 @@ function advanceClock(ms: number): void { vi.spyOn(Date, 'now').mockReturnValue(base + ms) } -function key( - actionId: 'dictation' | 'hands-free', - type: 'pressed' | 'released', - isDoublePress: boolean, -): KeyBindingTriggerPayload { - return { - actionId, - type, - isDoublePress, - holdMode: actionId === 'dictation', - timestamp: Date.now(), - durationMs: 0, - } as unknown as KeyBindingTriggerPayload -} - function screenContext(appName: string): { context: { appName: string; windowTitle: string; selectedText: null; capturedAt: number }; selectedTextAttempted: boolean } { return { context: { appName, windowTitle: 'w', selectedText: null, capturedAt: 0 }, selectedTextAttempted: false } } @@ -196,7 +167,6 @@ beforeEach(async () => { screen.captureSelectedText.mockResolvedValue('선택 문장') const mod = await import('../../../src/main/services/VoiceModeService') mod.resetVoiceModeServiceForTests() - getVoiceModeService = mod.getVoiceModeService createWithStore = mod.createVoiceModeServiceForTests }) diff --git a/apps/desktop/tests/main/update-policy-redteam-r3-5.test.ts b/apps/desktop/tests/main/update-policy-redteam-r3-5.test.ts index 7607819..2291bce 100644 --- a/apps/desktop/tests/main/update-policy-redteam-r3-5.test.ts +++ b/apps/desktop/tests/main/update-policy-redteam-r3-5.test.ts @@ -66,7 +66,10 @@ describe('parseUpdatePolicy — 안전 필드 fail-closed', () => { }) it('필드가 아예 없으면 기존처럼 기본값을 쓴다', () => { - const { killSwitch: _k, stagingPercentage: _s, ...rest } = BASE + // 두 필드를 뺀 정책 — 나머지는 BASE 그대로 + const rest = Object.fromEntries( + Object.entries(BASE).filter(([field]) => field !== 'killSwitch' && field !== 'stagingPercentage'), + ) const parsed = parseUpdatePolicy(rest) expect(parsed.killSwitch).toBe(DEFAULT_UPDATE_POLICY.killSwitch) expect(parsed.stagingPercentage).toBe(DEFAULT_UPDATE_POLICY.stagingPercentage) diff --git a/scripts/ci/verify-release-metadata.mjs b/scripts/ci/verify-release-metadata.mjs index 0536dea..b9f7a81 100644 --- a/scripts/ci/verify-release-metadata.mjs +++ b/scripts/ci/verify-release-metadata.mjs @@ -30,6 +30,7 @@ function loadSurfaces(readSurface = read) { updateFeed: readSurface('apps/desktop/src/main/update-feed.ts'), updatePolicySource: readSurface('apps/desktop/src/main/update-policy.ts'), updateService: readSurface('apps/desktop/src/main/services/UpdateService.ts'), + updateAdapters: readSurface('apps/desktop/src/main/services/update-adapters.ts'), publisher: readSurface('scripts/ci/publish-gitlab-release.mjs'), forgejoPublisher: readSurface('scripts/ci/publish-forgejo-release.mjs'), gitlab: readSurface('.gitlab-ci.yml'), @@ -166,14 +167,19 @@ function validate(surfaces) { surfaces.updatePolicySource.includes('isWithinRollout'), 'update_policy_runtime_logic_missing', ) + // 정책 판단(decideUpdate + isWithinRollout)은 update-policy.ts 의 evaluateUpdateOffer 가 묶는다. + // UpdateService 는 그 한 함수로 게이팅하고 킬 스위치를 직접 본다(2026-09 리팩터로 판단이 서비스 밖으로 옮겨졌다). fail( - surfaces.updateService.includes('decideUpdate') && - surfaces.updateService.includes('isWithinRollout') && + surfaces.updatePolicySource.includes('export function evaluateUpdateOffer') && + /evaluateUpdateOffer[\s\S]*decideUpdate\(/.test(surfaces.updatePolicySource) && + /evaluateUpdateOffer[\s\S]*isWithinRollout\(/.test(surfaces.updatePolicySource) && + surfaces.updateService.includes('evaluateUpdateOffer(') && /\bkillSwitch\b/.test(surfaces.updateService), 'update_service_policy_enforcement_missing', ) + // 차등(증분) 다운로드 제어는 UpdateService 가 위임하는 update-adapters.ts 의 전자 업데이터 어댑터에 있다 fail( - surfaces.updateService.includes('disableDifferentialDownload'), + surfaces.updateAdapters.includes('disableDifferentialDownload'), 'update_service_differential_control_missing', ) @@ -191,8 +197,10 @@ function validate(surfaces) { fail(surfaces.forgejoPublisher.includes('verifyPublicFile'), 'forgejo_publisher_public_verification_missing') fail(surfaces.forgejoPublisher.includes('update-policy.json'), 'forgejo_publisher_policy_upload_missing') fail(surfaces.forgejoPublisher.includes('CHANGELOG.md'), 'forgejo_publisher_changelog_gate_missing') + // 재게시 방지: 버전별 불변 경로의 원격 sha256 과 로컬을 비교해 다르면 중단한다(lib/immutable-package-guard.mjs) fail( - surfaces.forgejoPublisher.includes('assertVersionNotRepublished'), + surfaces.forgejoPublisher.includes('classifyImmutableAssets') && + surfaces.forgejoPublisher.includes('assertNoImmutableConflicts('), 'forgejo_publisher_rerelease_guard_missing', ) fail( @@ -309,7 +317,7 @@ if (process.argv.includes('--self-test')) { expectRejected( surfaces, (candidate) => { - candidate.forgejoPublisher = candidate.forgejoPublisher.replaceAll('assertVersionNotRepublished', 'uploadAnyway') + candidate.forgejoPublisher = candidate.forgejoPublisher.replaceAll('assertNoImmutableConflicts(', 'uploadAnyway(') }, 'forgejo_publisher_rerelease_guard_missing', )