Files
hermes/ui-tui/src/__tests__/markdown.test.ts
Brooklyn Nicholson 43eb1153e9 fix(tui): don't swallow Kimi/Qwen ~! ~? kaomoji as subscript spans
The inline markdown regex had `~([^~\s][^~]*?)~` for Pandoc-style subscript
(H~2~O, CO~2~). On models that decorate prose with kaomoji like `thing ~!`
and `cool ~?` — Kimi especially — the opener `~!` paired with the next
stray `~` on the line and dim-formatted everything between them with a
leading `_` character, mangling markdown output.

Tighten the pattern to short alphanumeric-only content (`~[A-Za-z0-9]{1,8}~`)
since real subscript never contains punctuation, spaces, or long runs.
Same tightening applied to stripInlineMarkup so width measurement stays
consistent. Classic CLI was unaffected because it renders these literally.
2026-04-21 17:34:48 -05:00

87 lines
3.6 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { AUDIO_DIRECTIVE_RE, INLINE_RE, MEDIA_LINE_RE, stripInlineMarkup } from '../components/markdown.js'
const matches = (text: string) => [...text.matchAll(INLINE_RE)].map(m => m[0])
describe('INLINE_RE emphasis', () => {
it('matches word-boundary italic/bold', () => {
expect(matches('say _hi_ there')).toEqual(['_hi_'])
expect(matches('very __bold__ move')).toEqual(['__bold__'])
expect(matches('(_paren_) and [_bracket_]')).toEqual(['_paren_', '_bracket_'])
})
it('keeps intraword underscores literal', () => {
const path = '/home/me/.hermes/cache/screenshots/browser_screenshot_ecc1c3feab.png'
expect(matches(path)).toEqual([])
expect(matches('snake_case_var and MY_CONST')).toEqual([])
expect(matches('foo__bar__baz')).toEqual([])
})
it('still matches asterisk emphasis intraword', () => {
expect(matches('a*b*c')).toEqual(['*b*'])
expect(matches('a**bold**c')).toEqual(['**bold**'])
})
it('matches short alphanumeric subscript (H~2~O, CO~2~, X~n~)', () => {
expect(matches('H~2~O')).toEqual(['~2~'])
expect(matches('CO~2~ levels')).toEqual(['~2~'])
expect(matches('the X~n~ term')).toEqual(['~n~'])
})
it('ignores kaomoji-style ~! and ~? punctuation', () => {
// Kimi / Qwen / GLM emit these as decorators and the whole span between
// two tildes used to get collapsed into one dim blob.
expect(matches('Aww ~! Building step by step, I love it ~!')).toEqual([])
expect(matches('cool ~? yeah ~?')).toEqual([])
expect(matches('mixed ~! and ~? flow')).toEqual([])
})
it('ignores tilde spans that contain spaces or punctuation', () => {
// Real subscript doesn't contain spaces; a tilde followed by words-then-
// tilde is almost always conversational. Matching it swallows text.
expect(matches('hello ~good idea~ there')).toEqual([])
expect(matches('x ~oh no!~ y')).toEqual([])
})
it('does not let strikethrough eat subscript', () => {
expect(matches('~~strike~~ and H~2~O')).toEqual(['~~strike~~', '~2~'])
})
})
describe('stripInlineMarkup', () => {
it('strips word-boundary emphasis only', () => {
expect(stripInlineMarkup('say _hi_ there')).toBe('say hi there')
expect(stripInlineMarkup('browser_screenshot_ecc.png')).toBe('browser_screenshot_ecc.png')
expect(stripInlineMarkup('__bold__ and foo__bar__')).toBe('bold and foo__bar__')
})
it('leaves ~!/~? kaomoji alone and still handles real subscript', () => {
expect(stripInlineMarkup('Yay ~! nice work ~!')).toBe('Yay ~! nice work ~!')
expect(stripInlineMarkup('H~2~O and CO~2~')).toBe('H_2O and CO_2')
})
})
describe('protocol sentinels', () => {
it('captures MEDIA: paths with surrounding quotes or backticks', () => {
expect('MEDIA:/tmp/a.png'.match(MEDIA_LINE_RE)?.[1]).toBe('/tmp/a.png')
expect(' MEDIA: /home/me/.hermes/cache/screenshots/browser_screenshot_ecc.png '.match(MEDIA_LINE_RE)?.[1]).toBe(
'/home/me/.hermes/cache/screenshots/browser_screenshot_ecc.png'
)
expect('`MEDIA:/tmp/a.png`'.match(MEDIA_LINE_RE)?.[1]).toBe('/tmp/a.png')
expect('"MEDIA:C:\\files\\a.png"'.match(MEDIA_LINE_RE)?.[1]).toBe('C:\\files\\a.png')
})
it('ignores MEDIA: tokens embedded in prose', () => {
expect('here is MEDIA:/tmp/a.png for you'.match(MEDIA_LINE_RE)).toBeNull()
expect('the media: section is empty'.match(MEDIA_LINE_RE)).toBeNull()
})
it('matches the [[audio_as_voice]] directive', () => {
expect(AUDIO_DIRECTIVE_RE.test('[[audio_as_voice]]')).toBe(true)
expect(AUDIO_DIRECTIVE_RE.test(' [[audio_as_voice]] ')).toBe(true)
expect(AUDIO_DIRECTIVE_RE.test('audio_as_voice')).toBe(false)
})
})