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).
181 lines
6.8 KiB
TypeScript
181 lines
6.8 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
describe('ConfigService suggestion tuning migration', () => {
|
|
afterEach(() => {
|
|
vi.doUnmock('electron-store')
|
|
vi.resetModules()
|
|
})
|
|
|
|
it('revision 3은 새 delay와 분당 상한만 바꾸고 기존 개인 설정을 보존한다', async () => {
|
|
const savedBindings = {
|
|
'suggestion-accept': [
|
|
{ device: 'keyboard' as const, code: 65, ctrl: true, alt: false, shift: false, meta: false }
|
|
]
|
|
}
|
|
const persisted = {
|
|
suggestionTuningRevision: 3,
|
|
suggestionTriggerDelayMs: 300,
|
|
suggestionMaxRequestsPerMinute: 12,
|
|
suggestionMinPrefixChars: 17,
|
|
suggestionDailyBudget: 777,
|
|
suggestionRequestTimeoutMs: 23000,
|
|
keyBindings: savedBindings
|
|
}
|
|
|
|
class TestStore<T extends Record<string, unknown>> {
|
|
store: T
|
|
|
|
constructor(options: { defaults: T }) {
|
|
this.store = { ...options.defaults, ...persisted } as T
|
|
}
|
|
|
|
get<K extends keyof T>(key: K): T[K] {
|
|
return this.store[key]
|
|
}
|
|
|
|
set<K extends keyof T>(key: K, value: T[K]): void {
|
|
this.store[key] = value
|
|
}
|
|
|
|
delete(key: string): void {
|
|
delete this.store[key as keyof T]
|
|
}
|
|
}
|
|
|
|
vi.doMock('electron-store', () => ({ default: TestStore }))
|
|
const { configGet, initConfigService, resetInMemoryConfig } = await import(
|
|
'../../../src/main/services/ConfigService'
|
|
)
|
|
await initConfigService()
|
|
|
|
expect(configGet('suggestionTuningRevision')).toBe(5)
|
|
expect(configGet('suggestionTriggerDelayMs')).toBe(600)
|
|
expect(configGet('suggestionMaxRequestsPerMinute')).toBe(6)
|
|
expect(configGet('suggestionMinPrefixChars')).toBe(17)
|
|
expect(configGet('suggestionDailyBudget')).toBe(777)
|
|
expect(configGet('suggestionRequestTimeoutMs')).toBe(23000)
|
|
// 옛 기본값이 아닌 커스터마이즈(Ctrl+A)는 revision 5 마이그레이션도 건드리지 않는다.
|
|
expect(configGet('keyBindings')['suggestion-accept']).toEqual(savedBindings['suggestion-accept'])
|
|
|
|
resetInMemoryConfig()
|
|
})
|
|
|
|
function makeTestStore<T extends Record<string, unknown>>(persisted: Partial<T>) {
|
|
return class TestStore {
|
|
store: T
|
|
|
|
constructor(options: { defaults: T }) {
|
|
this.store = { ...options.defaults, ...persisted } as T
|
|
}
|
|
|
|
get<K extends keyof T>(key: K): T[K] {
|
|
return this.store[key]
|
|
}
|
|
|
|
set<K extends keyof T>(key: K, value: T[K]): void {
|
|
this.store[key] = value
|
|
}
|
|
|
|
delete(key: string): void {
|
|
delete this.store[key as keyof T]
|
|
}
|
|
}
|
|
}
|
|
|
|
it('revision 5: 정확히 옛 기본값(accept=Ctrl+Alt+→, dismiss=Ctrl+Alt+←)이면 새 기본값으로 옮기고 페이지 이동을 그 자리에 채운다', async () => {
|
|
const OLD_ACCEPT = [{ device: 'keyboard' as const, code: 39, ctrl: true, alt: true, shift: false, meta: false }]
|
|
const OLD_NEXT = [{ device: 'keyboard' as const, code: 40, ctrl: true, alt: true, shift: false, meta: false }]
|
|
const OLD_PREV = [{ device: 'keyboard' as const, code: 38, ctrl: true, alt: true, shift: false, meta: false }]
|
|
const OLD_DISMISS = [{ device: 'keyboard' as const, code: 37, ctrl: true, alt: true, shift: false, meta: false }]
|
|
|
|
vi.doMock('electron-store', () => ({
|
|
default: makeTestStore({
|
|
suggestionTuningRevision: 4,
|
|
keyBindings: {
|
|
'suggestion-accept': OLD_ACCEPT,
|
|
'suggestion-next': OLD_NEXT,
|
|
'suggestion-prev': OLD_PREV,
|
|
'suggestion-dismiss': OLD_DISMISS
|
|
}
|
|
})
|
|
}))
|
|
const { configGet, initConfigService, resetInMemoryConfig } = await import(
|
|
'../../../src/main/services/ConfigService'
|
|
)
|
|
await initConfigService()
|
|
|
|
const bindings = configGet('keyBindings')
|
|
expect(bindings['suggestion-accept']).toEqual([
|
|
{ device: 'keyboard', code: 0x0d, ctrl: true, alt: true, shift: false, meta: false }
|
|
])
|
|
expect(bindings['suggestion-dismiss']).toEqual([
|
|
{ device: 'keyboard', code: 0x08, ctrl: true, alt: true, shift: false, meta: false }
|
|
])
|
|
// 옮기지 않는 액션은 그대로 남는다.
|
|
expect(bindings['suggestion-next']).toEqual(OLD_NEXT)
|
|
expect(bindings['suggestion-prev']).toEqual(OLD_PREV)
|
|
// accept/dismiss 가 비켜난 자리를 새 페이지 이동 액션이 충돌 없이 채운다.
|
|
expect(bindings['suggestion-page-next']).toEqual([
|
|
{ device: 'keyboard', code: 0x27, ctrl: true, alt: true, shift: false, meta: false }
|
|
])
|
|
expect(bindings['suggestion-page-prev']).toEqual([
|
|
{ device: 'keyboard', code: 0x25, ctrl: true, alt: true, shift: false, meta: false }
|
|
])
|
|
expect(configGet('suggestionTuningRevision')).toBe(5)
|
|
|
|
resetInMemoryConfig()
|
|
})
|
|
|
|
it('revision 5: 옛 기본값이 아닌 커스터마이즈는 절대 건드리지 않는다', async () => {
|
|
const CUSTOM_ACCEPT = [{ device: 'keyboard' as const, code: 0x41, ctrl: true, alt: false, shift: false, meta: false }]
|
|
const CUSTOM_DISMISS = [{ device: 'keyboard' as const, code: 0x44, ctrl: true, alt: false, shift: false, meta: false }]
|
|
|
|
vi.doMock('electron-store', () => ({
|
|
default: makeTestStore({
|
|
suggestionTuningRevision: 4,
|
|
keyBindings: {
|
|
'suggestion-accept': CUSTOM_ACCEPT,
|
|
'suggestion-dismiss': CUSTOM_DISMISS
|
|
}
|
|
})
|
|
}))
|
|
const { configGet, initConfigService, resetInMemoryConfig } = await import(
|
|
'../../../src/main/services/ConfigService'
|
|
)
|
|
await initConfigService()
|
|
|
|
const bindings = configGet('keyBindings')
|
|
expect(bindings['suggestion-accept']).toEqual(CUSTOM_ACCEPT)
|
|
expect(bindings['suggestion-dismiss']).toEqual(CUSTOM_DISMISS)
|
|
|
|
resetInMemoryConfig()
|
|
})
|
|
|
|
it('revision 5: 새 페이지 이동 기본값이 다른 액션과 충돌하면 바인딩 없이 둔다', async () => {
|
|
// suggestion-next 를 페이지-다음의 새 기본값(Ctrl+Alt+→)과 똑같이 커스터마이즈해 충돌을 만든다.
|
|
const CONFLICTING_NEXT = [{ device: 'keyboard' as const, code: 0x27, ctrl: true, alt: true, shift: false, meta: false }]
|
|
|
|
vi.doMock('electron-store', () => ({
|
|
default: makeTestStore({
|
|
suggestionTuningRevision: 4,
|
|
keyBindings: {
|
|
'suggestion-next': CONFLICTING_NEXT
|
|
}
|
|
})
|
|
}))
|
|
const { configGet, initConfigService, resetInMemoryConfig } = await import(
|
|
'../../../src/main/services/ConfigService'
|
|
)
|
|
await initConfigService()
|
|
|
|
const bindings = configGet('keyBindings')
|
|
expect(bindings['suggestion-page-next']).toEqual([])
|
|
// 충돌이 없는 page-prev 는 정상적으로 기본값을 받는다.
|
|
expect(bindings['suggestion-page-prev']).toEqual([
|
|
{ device: 'keyboard', code: 0x25, ctrl: true, alt: true, shift: false, meta: false }
|
|
])
|
|
expect(bindings['suggestion-next']).toEqual(CONFLICTING_NEXT)
|
|
|
|
resetInMemoryConfig()
|
|
})
|
|
})
|