import { describe, expect, it } from 'vitest' import { decideSuggestion, extractTerminalPromptPrefix, isTerminalApp } from '../src/input-intelligence' describe('terminal prompt extraction', () => { it('takes the Claude Code input line and ignores the status line below it', () => { const screen = [ '● Wrote 3 files', '', '╭──────────────────────────────────────╮', '│ > 오늘 회의 내용을 정리해서 │', '╰──────────────────────────────────────╯', ' ◑ OPUS 5 · 5 medium │ CTX ▕░░▏', ].join('\n') expect(extractTerminalPromptPrefix(screen)).toBe('오늘 회의 내용을 정리해서') }) it('joins wrapped continuation lines of a long prompt', () => { const screen = ['──────────', '> 긴 문장을 쓰다 보면', ' 줄이 바뀌어서 이어진다', '──────────', ' ? for shortcuts'].join('\n') expect(extractTerminalPromptPrefix(screen)).toBe('긴 문장을 쓰다 보면 줄이 바뀌어서 이어진다') }) it('reads shell prompts', () => { expect(extractTerminalPromptPrefix('PS C:\\Users\\me> git commit -m "고친')).toBe('git commit -m "고친') expect(extractTerminalPromptPrefix('C:\\work> dir')).toBe('dir') expect(extractTerminalPromptPrefix('me@host:~/app$ npm run')).toBe('npm run') expect(extractTerminalPromptPrefix('❯ cargo build')).toBe('cargo build') }) it('returns null when only output is on screen, and does not mistake percentages for prompts', () => { expect(extractTerminalPromptPrefix('Compiling...\n100% 완료\nDone in 3s')).toBeNull() expect(extractTerminalPromptPrefix('')).toBeNull() }) it('an empty prompt yields an empty prefix', () => { expect(extractTerminalPromptPrefix('output\n> ')).toBe('') }) it('recognises terminal apps', () => { expect(isTerminalApp('WindowsTerminal.exe')).toBe(true) expect(isTerminalApp('KakaoTalk.exe')).toBe(false) expect(isTerminalApp(null)).toBe(false) }) }) describe('a visible suggestion survives a typing pause', () => { const base = { enabled: true, modelAvailable: true, composing: false, hasSelection: false, isPassword: false, isEditable: true, appName: 'KakaoTalk.exe', excludedApps: [], editedSinceFocus: false, typedRecently: false, prefix: '내일 몇 시에 볼까', idleMs: 5000, triggerDelayMs: 100, minPrefixChars: 4, sinceLastRequestMs: 60_000, minIntervalMs: 5000, requestsThisMinute: 0, maxRequestsPerMinute: 6, requestsToday: 0, dailyBudget: 500, } it('keeps the overlay while the user reads and picks a candidate', () => { expect(decideSuggestion({ ...base, overlayVisible: true })).toEqual({ action: 'skip', reason: 'already-visible' }) }) it('still refuses to start a new suggestion without typing', () => { expect(decideSuggestion({ ...base, overlayVisible: false })).toEqual({ action: 'clear', reason: 'not-typing' }) }) })