Some checks failed
ci / 정본·보안·린트·타입·테스트 (push) Successful in 51s
ci / 모바일 린트·타입·Jest (push) Successful in 37s
ci / Supabase Edge Functions + Cloudflare Worker (push) Successful in 26s
ci / .NET API 서버 테스트 (push) Successful in 14s
deploy-site / deploy (push) Failing after 15s
ci / 워크스페이스 빌드 검증 (push) Failing after 11m7s
With the embedding server unavailable every chunk failed, yet the document was stored as indexed=true with 0 chunks, so the knowledge base listed it as searchable while queries could never match it. The red use-case test caught this; it had been written off as an environment failure. Now a run with zero embedded chunks leaves indexed=false and throws RAGEmbeddingFailed (surfaced by reindex, logged by addDocument). Tests that only hold on the Windows developer machine now declare it: the bundled SoX binary and PowerShell device discovery run on win32 only, and the sidecar venv test runs only when sidecar/.venv exists. The Linux Forgejo runner skips them instead of failing.
165 lines
5.6 KiB
TypeScript
165 lines
5.6 KiB
TypeScript
import { EventEmitter } from 'events'
|
|
import { readFileSync } from 'fs'
|
|
import { resolve } from 'path'
|
|
import { promisify } from 'util'
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
const childProcess = vi.hoisted(() => ({
|
|
exec: vi.fn(),
|
|
execFile: vi.fn(),
|
|
execFileAsync: vi.fn(),
|
|
spawn: vi.fn(),
|
|
}))
|
|
|
|
vi.mock('child_process', () => childProcess)
|
|
|
|
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(),
|
|
configSet: vi.fn(),
|
|
}))
|
|
|
|
vi.mock('../../../src/main/services/stt/STTManager', () => ({
|
|
getSTTManager: vi.fn(),
|
|
}))
|
|
|
|
vi.mock('../../../src/main/services/HistoryService', () => ({
|
|
getHistoryService: vi.fn(),
|
|
}))
|
|
|
|
vi.mock('../../../src/main/services/RuntimeProvisioner', () => ({
|
|
getRuntimeProvisioner: vi.fn(),
|
|
}))
|
|
|
|
vi.mock('../../../src/main/services/PremiumLLMService', () => ({
|
|
getPremiumLLMService: vi.fn(),
|
|
}))
|
|
|
|
function createProcess(args: unknown[]): EventEmitter & {
|
|
stdout: EventEmitter
|
|
stderr: EventEmitter
|
|
kill: ReturnType<typeof vi.fn>
|
|
} {
|
|
const process = Object.assign(new EventEmitter(), {
|
|
stdout: new EventEmitter(),
|
|
stderr: new EventEmitter(),
|
|
kill: vi.fn(),
|
|
})
|
|
|
|
queueMicrotask(() => {
|
|
if (args.includes('null')) {
|
|
process.stderr.emit('data', Buffer.from('Duration: 00:00:01.00'))
|
|
}
|
|
if (args.includes('pipe:1')) {
|
|
process.stdout.emit('data', Buffer.from('pcm'))
|
|
}
|
|
process.emit('close', 0)
|
|
})
|
|
|
|
return process
|
|
}
|
|
|
|
function mockSuccessfulExec(): void {
|
|
childProcess.exec.mockImplementation((...args: unknown[]) => {
|
|
const callback = args.find((arg): arg is (error: Error | null, stdout: string) => void => typeof arg === 'function')
|
|
callback?.(null, '[{"InstanceId":"device-1","FriendlyName":"Mic"}]')
|
|
})
|
|
}
|
|
|
|
function expectWindowsHideOnAllCalls(mock: { mock: { calls: unknown[][] } }): void {
|
|
for (const call of mock.mock.calls) {
|
|
const options = call.find(
|
|
(arg): arg is { windowsHide?: boolean } => typeof arg === 'object' && arg !== null && !Array.isArray(arg),
|
|
)
|
|
expect(options?.windowsHide).toBe(true)
|
|
}
|
|
}
|
|
|
|
beforeEach(() => {
|
|
vi.resetModules()
|
|
vi.clearAllMocks()
|
|
mockSuccessfulExec()
|
|
childProcess.spawn.mockImplementation((_command: unknown, args: unknown[]) => createProcess(args))
|
|
Reflect.set(
|
|
childProcess.execFile,
|
|
promisify.custom,
|
|
childProcess.execFileAsync,
|
|
)
|
|
childProcess.execFileAsync.mockResolvedValue({ stdout: 'TestApp\nTest window', stderr: '' })
|
|
})
|
|
|
|
describe('Windows child process visibility', () => {
|
|
it('hides the PowerShell SAPI process used for TTS playback', async () => {
|
|
const serviceModule = await import('../../../src/main/services/TTSPlaybackService')
|
|
serviceModule.resetTTSPlaybackServiceForTests()
|
|
const service = serviceModule.getTTSPlaybackService() as unknown as {
|
|
_speakOneWindows(text: string): Promise<void>
|
|
}
|
|
|
|
await service._speakOneWindows('테스트')
|
|
|
|
expect(childProcess.spawn).toHaveBeenCalledTimes(1)
|
|
expect(childProcess.spawn.mock.calls[0]?.[0]).toBe('powershell')
|
|
expect(childProcess.spawn.mock.calls[0]?.[2]).toMatchObject({ stdio: 'pipe', windowsHide: true })
|
|
})
|
|
|
|
it('hides cmd and PowerShell commands invoked by voice actions', async () => {
|
|
const serviceModule = await import('../../../src/main/services/VoiceActionService')
|
|
serviceModule.resetVoiceActionServiceForTests()
|
|
const service = serviceModule.getVoiceActionService() as unknown as {
|
|
_openApp(appName: string): Promise<void>
|
|
_simulateKeyboard(combo: string): Promise<void>
|
|
_runCommand(command: string): Promise<void>
|
|
}
|
|
|
|
await service._openApp('notepad')
|
|
await service._simulateKeyboard('volumeup')
|
|
await service._simulateKeyboard('volumedown')
|
|
await service._simulateKeyboard('volumemute')
|
|
await service._simulateKeyboard('ctrl+c')
|
|
await service._runCommand('echo test')
|
|
|
|
expect(childProcess.exec).toHaveBeenCalledTimes(6)
|
|
expect(childProcess.exec.mock.calls[0]?.[1]).toMatchObject({ shell: 'cmd.exe', windowsHide: true })
|
|
expect(childProcess.exec.mock.calls[5]?.[1]).toMatchObject({ timeout: 10000, windowsHide: true })
|
|
expectWindowsHideOnAllCalls(childProcess.exec)
|
|
})
|
|
|
|
// 장치 목록·활성 창 조회는 win32 분기에서만 PowerShell 을 부른다.
|
|
it.skipIf(process.platform !== 'win32')('hides Windows device discovery and active-window PowerShell calls', async () => {
|
|
const audioModule = await import('../../../src/main/services/AudioCaptureService')
|
|
const audioService = audioModule.getAudioCaptureService() as unknown as {
|
|
_getDevicesWindows(): Promise<unknown>
|
|
}
|
|
await audioService._getDevicesWindows()
|
|
|
|
const contextModule = await import('../../../src/main/services/ScreenContextService')
|
|
const contextService = contextModule.getScreenContextService()
|
|
await contextService.captureContext(false)
|
|
|
|
expect(childProcess.exec.mock.calls[0]?.[1]).toMatchObject({
|
|
encoding: 'utf8',
|
|
timeout: 3000,
|
|
windowsHide: true,
|
|
})
|
|
expect(childProcess.execFileAsync.mock.calls[0]?.[2]).toMatchObject({
|
|
timeout: 3000,
|
|
windowsHide: true,
|
|
})
|
|
})
|
|
|
|
it('declares hidden ffmpeg processes for conversion, duration, and chunk extraction', () => {
|
|
const source = readFileSync(
|
|
resolve(process.cwd(), 'src/main/services/FileTranscriptionService.ts'),
|
|
'utf8',
|
|
)
|
|
const ffmpegSpawns = source.match(
|
|
/spawn\(ffmpegPath, args, \{ stdio: \['pipe', 'pipe', 'pipe'\], windowsHide: true \}\)/g,
|
|
)
|
|
|
|
expect(ffmpegSpawns).toHaveLength(3)
|
|
})
|
|
})
|