d3ro-voice/apps/desktop/tests/main/utils/paths.test.ts
Yun Chan 2fe20fa7b5
Some checks failed
deploy-site / deploy (push) Failing after 39s
release / release-windows (push) Failing after 3m41s
portable-unsigned / portable-windows (push) Failing after 12m23s
release: ship v1.6.0 with paged suggestions and a cleaner phrase memory
Next-sentence suggestions now arrive one at a time up to twelve, shown three
per page with Ctrl+Alt+Up/Down to move, Left/Right to page, Enter to accept
and Esc to close; old default bindings migrate and the panel guide follows the
live bindings. The overlay is redesigned, stays put while candidates stream
and sits outside the input box when no caret is reported.

The personal phrase memory stops learning from terminals, code editors and
the coding-agent hub, ignores symbol-heavy lines and empty-field placeholders,
and prunes existing entries that break those rules.

Fixes suggestion keys starting dictation, installs stuck on a pre-1.5.0
speech engine without the focus endpoint, Ollama runner windows flashing
while typing, the speech engine starting twice, and cold-model timeouts.
Live captions can be dragged to a remembered position and show a waiting
notice until the first line arrives.

Bumps the product version to 1.6.0 (Android/iOS build 1060000).
2026-09-24 19:56:28 +09:00

97 lines
3.2 KiB
TypeScript

// tests/main/utils/paths.test.ts
// dev/packaged 경로 해석 테스트.
//
// 회귀 배경: electron-vite dev에서 app.getAppPath()가 `out/main`을 가리켜
// sidecar/SoX가 존재하지 않는 경로로 잡혔고, 결과적으로 로컬 전사와 녹음이
// 모두 실패했다(시스템 python/PATH 폴백). 여기서 그 해석을 고정한다.
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import { existsSync } from 'node:fs'
import path from 'node:path'
import { app } from 'electron'
import {
getAppRoot,
getSidecarCommand,
getSidecarBaseUrl,
getSoxPath,
resetPathCache,
} from '../../../src/main/utils/paths'
const desktopDir = path.resolve(__dirname, '..', '..', '..')
describe('paths (dev)', () => {
beforeEach(() => {
resetPathCache()
})
afterEach(() => {
vi.restoreAllMocks()
resetPathCache()
})
it('resolves the app root a level above the vite out/main bundle', () => {
// dev에서 app.getAppPath() = <desktop>/out/main 이다.
vi.mocked(app.getAppPath).mockReturnValue(path.join(desktopDir, 'out', 'main'))
resetPathCache()
expect(getAppRoot()).toBe(desktopDir)
})
it('prefers the bundled SoX binary over the system PATH', () => {
expect(getSoxPath()).toContain(path.join('resources', 'sox', 'sox'))
expect(existsSync(getSoxPath())).toBe(true)
expect(getSoxPath()).not.toBe('sox')
})
it('uses the sidecar virtualenv python when it exists', () => {
const launch = getSidecarCommand()
expect(launch.source).toBe('venv')
expect(launch.command).toContain('.venv')
expect(launch.args[0]).toBe(path.join(desktopDir, 'sidecar', 'main.py'))
expect(existsSync(launch.command)).toBe(true)
})
it('builds the sidecar base URL on the IPv4 loopback', () => {
expect(getSidecarBaseUrl(18765)).toBe('http://127.0.0.1:18765')
})
})
describe('paths (packaged)', () => {
beforeEach(() => {
resetPathCache()
Object.defineProperty(app, 'isPackaged', { value: true, configurable: true })
})
afterEach(() => {
Object.defineProperty(app, 'isPackaged', { value: false, configurable: true })
vi.restoreAllMocks()
resetPathCache()
})
it('fails loudly when the packaged sidecar bundle is missing', () => {
Object.defineProperty(process, 'resourcesPath', {
value: path.join(desktopDir, 'definitely-not-bundled'),
configurable: true,
})
expect(() => getSidecarCommand()).toThrowError(/로컬 음성 엔진이 아직 설치되지 않았습니다/)
})
it('uses the packaged sidecar executable when present', () => {
Object.defineProperty(process, 'resourcesPath', {
value: path.join(desktopDir, 'resources'),
configurable: true,
})
// resources/sox 는 커밋되어 있으므로 resources/sidecar/sidecar(.exe)도
// 같은 방식으로 배치된다. 존재하지 않는 플랫폼이면 번들 실패로 처리된다.
const exeSuffix = process.platform === 'win32' ? '.exe' : ''
const bundled = path.join(desktopDir, 'resources', 'sidecar', `sidecar${exeSuffix}`)
if (existsSync(bundled)) {
expect(getSidecarCommand().source).toBe('bundled')
} else {
expect(() => getSidecarCommand()).toThrow()
}
})
})