d3ro-voice/apps/desktop/tests/main/services/ConfigService.test.ts
Yun Chan 1b8fe445f3 fix(suggestion): stop generating mid-typing and let a pause bring a suggestion at once
With a 100 ms trigger left in the installed config, generation started in the
gaps between keystrokes; the next key ended the session silently, and that
session still counted against the 5 s minimum interval, so the moment the
user actually paused nothing came (rate-limited). The log showed a
generate-then-vanish cycle every 5-6 s.

The trigger delay now has a 500 ms floor and settings revision 6 resets a
stored value below it to 600 ms. A session ended because the user kept typing
no longer blocks the next request by the minimum interval (per-minute and
daily caps still apply), and that dismissal is logged.
2026-09-27 22:55:26 +09:00

186 lines
6.5 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(6)
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)
expect(configGet('suggestionTuningRevision')).toBe(6)
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 6: 하한(500ms) 미만으로 남은 멈춤 판단 시간만 기본값 600ms로 되돌린다', async () => {
for (const [stored, expected] of [
[100, 600],
[800, 800],
] as const) {
const persisted = { suggestionTuningRevision: 5, suggestionTriggerDelayMs: stored }
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(6)
expect(configGet('suggestionTriggerDelayMs')).toBe(expected)
resetInMemoryConfig()
vi.doUnmock('electron-store')
vi.resetModules()
}
})
})