feat(v027): AutostartDiagnostic — Windows registry 조회 + silent fallback

This commit is contained in:
altair823
2026-05-07 02:25:21 +09:00
parent 3a8137f334
commit 5f964aa2f5
2 changed files with 87 additions and 4 deletions

View File

@@ -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<AutostartState> {
const w = app.getLoginItemSettings({ args: ['--hidden'] });
const n = app.getLoginItemSettings();
@@ -24,6 +28,29 @@ export async function collectAutostartState(): Promise<AutostartState> {
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<string | null> {
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);
});
});
}

View File

@@ -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();
});
});