release: ship v1.5.0 with on-device writing suggestions
Adds next-sentence suggestions while typing, weekly input insights and a personal phrase memory to the desktop app, and fixes custom instructions so they process the text instead of inserting the instruction's own wording. Local model requests are now bounded and individually cancellable. Bumps the product version to 1.5.0 (Android/iOS build 1050000), refreshes the landing and web download links, and records the new INPUT feature rows and the open verification gaps in the infrastructure map.
This commit is contained in:
parent
99f06c253c
commit
5c11ee2fde
104 changed files with 14410 additions and 174 deletions
|
|
@ -0,0 +1,164 @@
|
|||
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)
|
||||
})
|
||||
|
||||
it('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)
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue