147 lines
6.5 KiB
TypeScript
147 lines
6.5 KiB
TypeScript
// tests/main/services/screen-context-redteam-r2-7.test.ts
|
|
// ScreenContextService + ForegroundWindowPort 회귀 테스트:
|
|
// - 활성 창 조회는 포트(koffi/osascript 어댑터)에 맡기고 PowerShell 을 띄우지 않는다
|
|
// - Windows 어댑터는 실제 앱 이름을 돌려준다 (예전 PowerShell 경로는 $PID 버그로 늘 'powershell')
|
|
// - 콘솔/터미널이 포그라운드면 복사 단축키를 보내지 않는다 (SIGINT·입력 줄 지우기 방지)
|
|
// - macOS 에선 ⌘+C 를 보낸다
|
|
|
|
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
import { clipboard } from 'electron'
|
|
import type { ForegroundWindowInfo } from '../../../src/main/utils/win32-foreground'
|
|
|
|
vi.mock('../../../src/main/services/LoggerService', () => ({
|
|
getLogger: () => ({ info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() }),
|
|
}))
|
|
|
|
vi.mock('../../../src/main/services/ConfigService', () => ({
|
|
configGet: vi.fn(() => true),
|
|
configSet: vi.fn(),
|
|
}))
|
|
|
|
vi.mock('../../../src/main/services/modifier-state', () => ({
|
|
waitForModifiersReleased: vi.fn(async () => true),
|
|
}))
|
|
|
|
const nut = vi.hoisted(() => ({ pressKey: vi.fn(async () => undefined), releaseKey: vi.fn(async () => undefined) }))
|
|
vi.mock('@nut-tree-fork/nut-js', () => ({
|
|
keyboard: nut,
|
|
Key: { LeftControl: 'LeftControl', LeftSuper: 'LeftSuper', C: 'C' },
|
|
}))
|
|
|
|
const childProcess = vi.hoisted(() => ({ execFile: vi.fn(), exec: vi.fn(), spawn: vi.fn() }))
|
|
vi.mock('child_process', async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import('child_process')>()
|
|
return { ...actual, ...childProcess }
|
|
})
|
|
|
|
type ScreenModule = typeof import('../../../src/main/services/ScreenContextService')
|
|
type PortModule = typeof import('../../../src/main/services/foreground-window-port')
|
|
let screenMod: ScreenModule
|
|
let portMod: PortModule
|
|
|
|
Object.assign(clipboard, { clear: vi.fn() })
|
|
|
|
function fakePort(appName: string | null, windowTitle: string | null = 'Title'): { current: ReturnType<typeof vi.fn> } {
|
|
return { current: vi.fn(async () => ({ appName, windowTitle })) }
|
|
}
|
|
|
|
function info(patch: Partial<ForegroundWindowInfo>): ForegroundWindowInfo {
|
|
return { hwnd: 1, title: '', processId: 1, appName: '', appPath: null, bounds: null, ...patch }
|
|
}
|
|
|
|
beforeEach(async () => {
|
|
vi.resetModules()
|
|
vi.clearAllMocks()
|
|
screenMod = await import('../../../src/main/services/ScreenContextService')
|
|
portMod = await import('../../../src/main/services/foreground-window-port')
|
|
})
|
|
|
|
describe('captureContext — ForegroundWindowPort', () => {
|
|
it('포트가 준 앱·창 정보를 쓰고 자식 프로세스를 띄우지 않는다', async () => {
|
|
const port = fakePort('Code', 'main.ts - D3ROVoice')
|
|
const svc = new screenMod.ScreenContextService({ foreground: port, platform: 'win32' })
|
|
|
|
const { context, selectedTextAttempted } = await svc.captureContext(false)
|
|
|
|
expect(context.appName).toBe('Code')
|
|
expect(context.windowTitle).toBe('main.ts - D3ROVoice')
|
|
expect(selectedTextAttempted).toBe(false)
|
|
expect(childProcess.execFile).not.toHaveBeenCalled()
|
|
expect(childProcess.spawn).not.toHaveBeenCalled()
|
|
expect(svc.buildContextPrompt(context)).toContain('활성 앱: Code')
|
|
})
|
|
|
|
it('포트가 실패해도 컨텍스트 캡처는 계속된다', async () => {
|
|
const port = { current: vi.fn(async () => { throw new Error('ffi') }) }
|
|
const svc = new screenMod.ScreenContextService({ foreground: port, platform: 'win32' })
|
|
const { context } = await svc.captureContext(false)
|
|
expect(context.appName).toBeNull()
|
|
expect(svc.buildContextPrompt(context)).toBe('')
|
|
})
|
|
})
|
|
|
|
describe('Windows 포그라운드 어댑터 (koffi)', () => {
|
|
it('실행 파일 이름에서 .exe 를 떼어 실제 앱 이름을 돌려준다', async () => {
|
|
const port = portMod.createWin32ForegroundWindowPort(() =>
|
|
info({ appName: 'Code.exe', title: 'main.ts', appPath: 'C:\\Code\\Code.exe' }),
|
|
)
|
|
await expect(port.current()).resolves.toEqual({ appName: 'Code', windowTitle: 'main.ts' })
|
|
})
|
|
|
|
it('빈 값과 조회 실패는 null 이다', async () => {
|
|
await expect(portMod.createWin32ForegroundWindowPort(() => info({})).current()).resolves.toEqual({
|
|
appName: null,
|
|
windowTitle: null,
|
|
})
|
|
await expect(portMod.createWin32ForegroundWindowPort(() => null).current()).resolves.toEqual({
|
|
appName: null,
|
|
windowTitle: null,
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('선택 텍스트 캡처 — 복사 단축키 안전성', () => {
|
|
it.each(['WindowsTerminal', 'conhost', 'pwsh', 'Terminal', 'iTerm2', 'WindowsTerminal.exe'])(
|
|
'포그라운드가 터미널(%s)이면 복사 단축키를 보내지 않는다',
|
|
async (appName) => {
|
|
const svc = new screenMod.ScreenContextService({ foreground: fakePort(appName), platform: 'win32' })
|
|
await expect(svc.captureSelectedText()).resolves.toBeNull()
|
|
expect(nut.pressKey).not.toHaveBeenCalled()
|
|
expect(clipboard.clear).not.toHaveBeenCalled()
|
|
},
|
|
)
|
|
|
|
it('Windows 에선 Ctrl+C 를 보낸다', async () => {
|
|
vi.mocked(clipboard.readText).mockReturnValueOnce('').mockReturnValueOnce('선택')
|
|
const svc = new screenMod.ScreenContextService({ foreground: fakePort('Code'), platform: 'win32' })
|
|
await expect(svc.captureSelectedText()).resolves.toBe('선택')
|
|
expect(nut.pressKey).toHaveBeenCalledWith('LeftControl', 'C')
|
|
})
|
|
|
|
it('macOS 에선 ⌘+C 를 보낸다', async () => {
|
|
vi.mocked(clipboard.readText).mockReturnValueOnce('').mockReturnValueOnce('선택')
|
|
const svc = new screenMod.ScreenContextService({ foreground: fakePort('Notes'), platform: 'darwin' })
|
|
await expect(svc.captureSelectedText()).resolves.toBe('선택')
|
|
expect(nut.pressKey).toHaveBeenCalledWith('LeftSuper', 'C')
|
|
expect(nut.pressKey).not.toHaveBeenCalledWith('LeftControl', 'C')
|
|
})
|
|
|
|
it('captureContext(true) 도 터미널이면 선택 텍스트를 건너뛴다', async () => {
|
|
const port = fakePort('WindowsTerminal')
|
|
const svc = new screenMod.ScreenContextService({ foreground: port, platform: 'win32' })
|
|
const { context, selectedTextAttempted } = await svc.captureContext(true)
|
|
expect(selectedTextAttempted).toBe(true)
|
|
expect(context.selectedText).toBeNull()
|
|
expect(nut.pressKey).not.toHaveBeenCalled()
|
|
// 포그라운드는 한 번만 조회한다
|
|
expect(port.current).toHaveBeenCalledTimes(1)
|
|
})
|
|
})
|
|
|
|
describe('isTerminalHost', () => {
|
|
it('대소문자·확장자와 무관하게 판별하고, 일반 앱은 아니다', () => {
|
|
expect(portMod.isTerminalHost('WINDOWSTERMINAL.EXE')).toBe(true)
|
|
expect(portMod.isTerminalHost('Code')).toBe(false)
|
|
expect(portMod.isTerminalHost(null)).toBe(false)
|
|
})
|
|
})
|