From 5f964aa2f5068e210058088c778a42769d9e4bec Mon Sep 17 00:00:00 2001 From: altair823 Date: Thu, 7 May 2026 02:25:21 +0900 Subject: [PATCH] =?UTF-8?q?feat(v027):=20AutostartDiagnostic=20=E2=80=94?= =?UTF-8?q?=20Windows=20registry=20=EC=A1=B0=ED=9A=8C=20+=20silent=20fallb?= =?UTF-8?q?ack?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/services/AutostartDiagnostic.ts | 29 ++++++++++- tests/unit/AutostartDiagnostic.test.ts | 62 ++++++++++++++++++++++-- 2 files changed, 87 insertions(+), 4 deletions(-) diff --git a/src/main/services/AutostartDiagnostic.ts b/src/main/services/AutostartDiagnostic.ts index c3921ab..c11a975 100644 --- a/src/main/services/AutostartDiagnostic.ts +++ b/src/main/services/AutostartDiagnostic.ts @@ -1,4 +1,5 @@ import electron from 'electron'; +import { execFile } from 'node:child_process'; const { app } = electron; /** @@ -16,6 +17,9 @@ export interface AutostartState { registryValue?: string | null; } +const WIN_REGISTRY_PATH = 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run'; +const WIN_REGISTRY_KEY = 'Inkling'; + export async function collectAutostartState(): Promise { const w = app.getLoginItemSettings({ args: ['--hidden'] }); const n = app.getLoginItemSettings(); @@ -24,6 +28,29 @@ export async function collectAutostartState(): Promise { noArgs: { openAtLogin: n.openAtLogin, executableWillLaunchAtLogin: n.executableWillLaunchAtLogin }, execPath: process.execPath }; - // Task 20 — Windows registry 조회 (HKCU\...\Run\Inkling) 는 다음 task 에서 추가. + if (process.platform === 'win32') { + state.registryPath = `${WIN_REGISTRY_PATH}\\${WIN_REGISTRY_KEY}`; + state.registryValue = await readRegistrySilent(); + } return state; } + +/** + * `reg query` 로 HKCU\\...\\Run\\Inkling 의 값을 조회. + * 키가 없으면 reg.exe 가 exit 1 → silent fallback (null). + * + * promisify(execFile) 대신 직접 Promise 로 wrapping — 테스트에서 vi.mock 이 + * `util.promisify.custom` symbol 을 보전하지 못해 stdout 이 undefined 가 되는 이슈 회피. + */ +function readRegistrySilent(): Promise { + return new Promise((resolve) => { + execFile('reg', ['query', WIN_REGISTRY_PATH, '/v', WIN_REGISTRY_KEY], (err, stdout) => { + if (err) { + resolve(null); + return; + } + const m = stdout.match(/REG_SZ\s+(.+)/); + resolve(m && m[1] ? m[1].trim() : null); + }); + }); +} diff --git a/tests/unit/AutostartDiagnostic.test.ts b/tests/unit/AutostartDiagnostic.test.ts index 4d28bce..014ffe0 100644 --- a/tests/unit/AutostartDiagnostic.test.ts +++ b/tests/unit/AutostartDiagnostic.test.ts @@ -1,21 +1,38 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -const { mockApp } = vi.hoisted(() => ({ - mockApp: { getLoginItemSettings: vi.fn() } +const { mockApp, mockExecFile } = vi.hoisted(() => ({ + mockApp: { getLoginItemSettings: vi.fn() }, + mockExecFile: vi.fn() })); vi.mock('electron', () => ({ default: { app: mockApp } })); +vi.mock('node:child_process', () => ({ + execFile: mockExecFile +})); + import { collectAutostartState } from '../../src/main/services/AutostartDiagnostic'; +const ORIGINAL_PLATFORM = process.platform; + +function setPlatform(p: NodeJS.Platform): void { + Object.defineProperty(process, 'platform', { value: p, configurable: true }); +} + describe('AutostartDiagnostic — collectAutostartState', () => { beforeEach(() => { mockApp.getLoginItemSettings.mockReset(); + mockExecFile.mockReset(); + }); + + afterEach(() => { + setPlatform(ORIGINAL_PLATFORM); }); it('returns withArgs / noArgs / execPath structure', async () => { + setPlatform('darwin'); mockApp.getLoginItemSettings .mockReturnValueOnce({ openAtLogin: true, executableWillLaunchAtLogin: true }) .mockReturnValueOnce({ openAtLogin: false, executableWillLaunchAtLogin: true }); @@ -28,6 +45,7 @@ describe('AutostartDiagnostic — collectAutostartState', () => { }); it('passes args=["--hidden"] for the first call, no args for the second', async () => { + setPlatform('darwin'); mockApp.getLoginItemSettings .mockReturnValueOnce({ openAtLogin: true, executableWillLaunchAtLogin: true }) .mockReturnValueOnce({ openAtLogin: true, executableWillLaunchAtLogin: true }); @@ -37,4 +55,42 @@ describe('AutostartDiagnostic — collectAutostartState', () => { expect(mockApp.getLoginItemSettings).toHaveBeenNthCalledWith(1, { args: ['--hidden'] }); expect(mockApp.getLoginItemSettings).toHaveBeenNthCalledWith(2); }); + + it('non-win32: does not set registryPath/registryValue', async () => { + setPlatform('darwin'); + mockApp.getLoginItemSettings.mockReturnValue({ openAtLogin: true, executableWillLaunchAtLogin: true }); + + const state = await collectAutostartState(); + + expect(state.registryPath).toBeUndefined(); + expect(state.registryValue).toBeUndefined(); + expect(mockExecFile).not.toHaveBeenCalled(); + }); + + it('Windows: returns registryPath + registryValue when reg.exe succeeds', async () => { + setPlatform('win32'); + mockApp.getLoginItemSettings.mockReturnValue({ openAtLogin: true, executableWillLaunchAtLogin: true }); + mockExecFile.mockImplementation((_cmd: string, _args: string[], cb: (err: Error | null, stdout: string, stderr: string) => void) => { + cb(null, '\r\nHKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Run\r\n Inkling REG_SZ "C:\\Users\\u\\Inkling.exe" --hidden\r\n', ''); + }); + + const state = await collectAutostartState(); + + expect(state.registryPath).toBe('HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run\\Inkling'); + expect(state.registryValue).toContain('Inkling.exe'); + expect(state.registryValue).toContain('--hidden'); + }); + + it('Windows: silent fallback on reg.exe error', async () => { + setPlatform('win32'); + mockApp.getLoginItemSettings.mockReturnValue({ openAtLogin: true, executableWillLaunchAtLogin: true }); + mockExecFile.mockImplementation((_cmd: string, _args: string[], cb: (err: Error | null, stdout: string, stderr: string) => void) => { + cb(new Error('not found'), '', ''); + }); + + const state = await collectAutostartState(); + + expect(state.registryPath).toBe('HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run\\Inkling'); + expect(state.registryValue).toBeNull(); + }); });