diff --git a/src/main/ai-vault/session-scanner-first-user-prompt.test.ts b/src/main/ai-vault/session-scanner-first-user-prompt.test.ts new file mode 100644 index 00000000000..47235cbce12 --- /dev/null +++ b/src/main/ai-vault/session-scanner-first-user-prompt.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, it } from 'vitest' +import { normalizeFullFirstUserPromptText } from './session-scanner-first-user-prompt' + +// Mirrors FULL_FIRST_USER_PROMPT_SAFETY_LIMIT in the module under test. +const SAFETY_LIMIT = 256 * 1024 + +describe('AI Vault full first-user-prompt normalization', () => { + it('drops an astral char straddling the safety limit instead of splitting it', () => { + const result = normalizeFullFirstUserPromptText(`${'a'.repeat(SAFETY_LIMIT - 1)}😀tail`) + + expect(result).toHaveLength(SAFETY_LIMIT - 1) + expect(result?.endsWith('a')).toBe(true) + expect(hasUnpairedSurrogate(result ?? '')).toBe(false) + }) + + it('keeps a prompt shorter than the safety limit intact', () => { + expect(normalizeFullFirstUserPromptText('ship it 😀')).toBe('ship it 😀') + }) +}) + +function hasUnpairedSurrogate(value: string): boolean { + for (let index = 0; index < value.length; index += 1) { + const code = value.charCodeAt(index) + const isHigh = code >= 0xd800 && code <= 0xdbff + const isLow = code >= 0xdc00 && code <= 0xdfff + if (isHigh) { + const next = value.charCodeAt(index + 1) + if (!(next >= 0xdc00 && next <= 0xdfff)) { + return true + } + index += 1 + continue + } + if (isLow) { + return true + } + } + return false +} diff --git a/src/main/ai-vault/session-scanner-grok-parser.test.ts b/src/main/ai-vault/session-scanner-grok-parser.test.ts index 8891b4d7212..bdbc19db468 100644 --- a/src/main/ai-vault/session-scanner-grok-parser.test.ts +++ b/src/main/ai-vault/session-scanner-grok-parser.test.ts @@ -41,6 +41,16 @@ describe('AI Vault Grok session parser', () => { expect(result?.endsWith('...')).toBe(true) }) + it('drops an astral char straddling the preview scan cap instead of splitting it', () => { + // Hidden context is skipped by the fold, so the 4096-code-unit scan cap lands + // mid-emoji while the visible text stays well under the 220-char preview cap. + const hidden = `${'x'.repeat(4057)}` + const result = extractGrokContentText(`${hidden}ask😀tail`) + + expect(hidden).toHaveLength(4092) + expect(result).toBe('ask') + }) + it('stores the unwrapped user_query as firstUserPrompt under full capture', async () => { const root = await mkdtemp(join(tmpdir(), 'orca-ai-vault-grok-first-')) tempRoots.push(root)