// 제안 정책 회귀 (r3-1): // - 케어렛을 모르는 필드에서 문서 중간을 고쳤으면 제안을 닫는다 (caret-unknown) // - 단어를 끝낸 뒤(뒤 공백) 로컬 기억이 단어 조각을 붙이지 않는다 // - 학습 제외 앱은 출처와 무관하게 학습하지 않는다 import { describe, expect, it } from 'vitest' import { decideSuggestion, isEditAtTextEnd, shouldLearnFromApp, shouldPersistSuggestionPrefix, type SuggestionPolicyInput } from '../src/input-intelligence' import { buildLocalSuggestionCandidateEntries, joinSuggestion, normalizeSessionPrefix } from '../src/suggestion-text' function policy(overrides: Partial = {}): SuggestionPolicyInput { return { enabled: true, modelAvailable: true, overlayVisible: false, composing: false, hasSelection: false, isPassword: false, isEditable: true, appName: 'notepad.exe', excludedApps: [], editedSinceFocus: true, typedRecently: true, prefix: '오늘 회의에서 논의한 내용을 정리해서', idleMs: 2000, triggerDelayMs: 600, minPrefixChars: 8, sinceLastRequestMs: 60_000, minIntervalMs: 5000, requestsThisMinute: 0, maxRequestsPerMinute: 12, requestsToday: 0, dailyBudget: 500, ...overrides } } describe('케어렛 신뢰도', () => { it('문서 끝에서 쓰거나 지운 편집만 끝 편집으로 본다', () => { expect(isEditAtTextEnd('abc', 'abcd')).toBe(true) expect(isEditAtTextEnd('abc', 'ab')).toBe(true) expect(isEditAtTextEnd('abc', 'abc')).toBe(true) expect(isEditAtTextEnd('첫 문장. 둘째 문장.', '첫 문장이. 둘째 문장.')).toBe(false) expect(isEditAtTextEnd('aXb', 'ab')).toBe(false) }) it('caretReliable=false 면 요청하지 않고 떠 있는 세션도 닫는다', () => { expect(decideSuggestion(policy({ caretReliable: false }))).toEqual({ action: 'clear', reason: 'caret-unknown' }) expect(decideSuggestion(policy({ caretReliable: false, overlayVisible: true }))).toEqual({ action: 'clear', reason: 'caret-unknown' }) }) it('생략하거나 true 면 기존처럼 요청한다', () => { expect(decideSuggestion(policy()).action).toBe('request') expect(decideSuggestion(policy({ caretReliable: true })).action).toBe('request') }) }) describe('단어를 끝낸 뒤 로컬 기억 이음새', () => { const hints = (phrase: string) => ({ continuationHints: [], relatedHints: [], phraseHints: [phrase] }) it("'내일 회의 ' 뒤에 '록 공유드립니다' 조각을 만들지 않는다", () => { const raw = '내일 회의 ' const entries = buildLocalSuggestionCandidateEntries(normalizeSessionPrefix(raw), hints('회의록 공유드립니다'), 3, 160, { wordFinished: true }) expect(entries.map((entry) => entry.text)).not.toContain('록 공유드립니다') }) it("'check the ' 뒤에 'n we can ship' 조각을 만들지 않는다 (원문을 넘기면 자동 판단)", () => { const entries = buildLocalSuggestionCandidateEntries('check the ', hints('then we can ship'), 3, 160) expect(entries.map((entry) => entry.text)).not.toContain('n we can ship') }) it('단어 중간에서 멈췄으면 기존처럼 붙인다', () => { const entries = buildLocalSuggestionCandidateEntries('내일 회의', hints('회의록 공유드립니다'), 3, 160) expect(entries[0]).toEqual({ text: '록 공유드립니다', attachToPrefix: true }) expect('내일 회의' + joinSuggestion('내일 회의', entries[0].text, 'attach')).toBe('내일 회의록 공유드립니다') }) }) describe('학습 제외 앱 정본', () => { it('터미널·에디터는 출처와 무관하게 학습하지 않는다', () => { expect(shouldLearnFromApp('WindowsTerminal.exe')).toBe(false) expect(shouldLearnFromApp('Code.exe')).toBe(false) expect(shouldLearnFromApp('KakaoTalk.exe')).toBe(true) expect(shouldLearnFromApp(null)).toBe(true) }) it('제안 접두 저장도 같은 규칙을 쓴다', () => { expect(shouldPersistSuggestionPrefix({ learnTypedText: true, appName: 'WindowsTerminal.exe' })).toBe(false) expect(shouldPersistSuggestionPrefix({ learnTypedText: true, appName: 'KakaoTalk.exe' })).toBe(true) }) })