From f505a03d347732efa5c9ceb6431a5a5be8f73856 Mon Sep 17 00:00:00 2001 From: Yun Chan Date: Mon, 28 Sep 2026 20:55:11 +0900 Subject: [PATCH] fix(ci): point release-metadata checks at the relocated update and publish guards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI run 121 failed at "버전·계약 정본 대조": earlier refactors moved update gating from UpdateService into update-policy.ts (evaluateUpdateOffer ties decideUpdate + isWithinRollout), the differential-download switch into update-adapters.ts, and the Forgejo re-release guard into lib/immutable-package-guard.mjs (sha256 comparison, abort on conflict). The checks still looked for the old markers in the old files. They now verify the same guarantees where the code lives, and the self-test's negative case targets assertNoImmutableConflicts. Also clears the lint gate: a control-character regex in Keycap (range now starts at U+0020) and unused helpers in two red-team tests. --- .../renderer/components/keybinding/Keycap.tsx | 2 +- ...voice-recording-store-redteam-r3-1.test.ts | 30 ------------------- .../main/update-policy-redteam-r3-5.test.ts | 5 +++- scripts/ci/verify-release-metadata.mjs | 18 +++++++---- 4 files changed, 18 insertions(+), 37 deletions(-) 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', )