Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 117 additions & 0 deletions apps/desktop/src/lib/markdown-code.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,30 @@ import { describe, expect, it } from 'vitest'

import { isLikelyProseCodeBlock, isLikelyProseFence, isLikelyStructuredText } from './markdown-code'

// Explicit-tag regression matrix (XpycT's four-block repro on Windows 11,
// Hermes v0.19.1): text/markdown/gdscript/yaml fences whose content looks
// prose-like (>=3 lines, no code signals) must stay code blocks. Only bare
// fences ('') still fall through to the prose heuristic.
const LOREM_3 = ['lorem ipsum dolor sit amet', 'consectetur adipiscing elit', 'sed do eiusmod tempor'].join('\n')
const SENTENCE_3 = [
'The quarterly report shows steady growth across all sectors.',
'Regional performance exceeded expectations by twelve percent.',
'Customer retention rates improved for the third consecutive month.'
].join('\n')
const MARKDOWN_BLOCK = ['# Deployment notes', '', 'This block should stay fenced'].join('\n')
const YAML_LIST = ['providers:', ' - name: deepseek', ' - name: openai'].join('\n')
const BULLET_2 = ['- item one', '- item two'].join('\n')

// Stone441 macOS repro (2026-08-14): long `text`/`prompt` fences with
// numbered Chinese prose were downgraded to rendered prose. Numbered lines
// ("1. 中文…") match proseLineCount's ASCII-leading regex, so they trip the
// prose heuristic; the explicit-tag guard must keep them fenced.
const CHINESE_NUMBERED = [
'1. 这是一个较长的中文提示词,包含编号和具体步骤说明。',
'2. 第二行继续描述用户希望保留的完整提示词内容。',
'3. 第三行用于验证换行、编号和空格在代码块内保持原样。'
].join('\n')

describe('isLikelyProseCodeBlock', () => {
it('detects prose that Streamdown mislabels as an unknown language', () => {
expect(
Expand Down Expand Up @@ -35,6 +59,57 @@ describe('isLikelyProseCodeBlock', () => {
it('keeps an .env-style dump fenced', () => {
expect(isLikelyProseCodeBlock('', ['API_KEY=abc123', 'PORT=8080', 'DEBUG=true'].join('\n'))).toBe(false)
})

it('keeps text fences as code even with zero code signals', () => {
expect(isLikelyProseCodeBlock('text', LOREM_3)).toBe(false)
expect(isLikelyProseCodeBlock('text', SENTENCE_3)).toBe(false)
})

it('keeps plain/plaintext fences as code', () => {
expect(isLikelyProseCodeBlock('plain', LOREM_3)).toBe(false)
expect(isLikelyProseCodeBlock('plaintext', SENTENCE_3)).toBe(false)
})

it('keeps markdown/md fences as code instead of rich-rendering them', () => {
expect(isLikelyProseCodeBlock('markdown', MARKDOWN_BLOCK)).toBe(false)
expect(isLikelyProseCodeBlock('md', SENTENCE_3)).toBe(false)
})

it('keeps gdscript as code (non-COMMON explicit tag)', () => {
expect(isLikelyProseCodeBlock('gdscript', SENTENCE_3)).toBe(false)
})

it('keeps yaml bullet lists as code', () => {
expect(isLikelyProseCodeBlock('yaml', YAML_LIST)).toBe(false)
})

// Whole-class regression (spfcraze triage, 2026-08-14): non-COMMON explicit
// tags with bullet-list bodies were prose-classified by the bullet heuristic
// before the explicit-tag guard was reached. gdscript/zsh are non-COMMON,
// text/plain/plaintext are NON_CODE (not COMMON) — all must stay code.
it('keeps gdscript bullet lists as code (spfcraze repro)', () => {
expect(isLikelyProseCodeBlock('gdscript', BULLET_2)).toBe(false)
})

it('keeps zsh bullet lists as code (spfcraze repro)', () => {
expect(isLikelyProseCodeBlock('zsh', BULLET_2)).toBe(false)
})

it('keeps NON_CODE-family bullet lists as code (text/plain/plaintext)', () => {
expect(isLikelyProseCodeBlock('text', BULLET_2)).toBe(false)
expect(isLikelyProseCodeBlock('plain', BULLET_2)).toBe(false)
expect(isLikelyProseCodeBlock('plaintext', BULLET_2)).toBe(false)
})

// Stone441 macOS repro: numbered Chinese prose inside text/prompt fences
// must stay code blocks (the numbered lines trip the prose heuristic).
it('keeps text fences with numbered Chinese prose as code (Stone441)', () => {
expect(isLikelyProseCodeBlock('text', CHINESE_NUMBERED)).toBe(false)
})

it('keeps prompt fences with numbered Chinese prose as code (Stone441)', () => {
expect(isLikelyProseCodeBlock('prompt', CHINESE_NUMBERED)).toBe(false)
})
})

describe('isLikelyStructuredText', () => {
Expand Down Expand Up @@ -92,4 +167,46 @@ describe('isLikelyProseFence', () => {
)
).toBe(true)
})

it('keeps text fences as code', () => {
expect(isLikelyProseFence('text', LOREM_3)).toBe(false)
expect(isLikelyProseFence('text', SENTENCE_3)).toBe(false)
})

it('keeps plain/plaintext fences as code', () => {
expect(isLikelyProseFence('plain', LOREM_3)).toBe(false)
expect(isLikelyProseFence('plaintext', SENTENCE_3)).toBe(false)
})

it('keeps markdown/md fences as code instead of rich-rendering them', () => {
expect(isLikelyProseFence('markdown', MARKDOWN_BLOCK)).toBe(false)
expect(isLikelyProseFence('md', SENTENCE_3)).toBe(false)
})

it('keeps gdscript as code (non-COMMON explicit tag)', () => {
expect(isLikelyProseFence('gdscript', SENTENCE_3)).toBe(false)
})

it('keeps yaml bullet lists as code', () => {
expect(isLikelyProseFence('yaml', YAML_LIST)).toBe(false)
})

// Whole-class regression (spfcraze triage, 2026-08-14): same bullet-list
// coverage on the fence layer — non-COMMON explicit tags must stay code.
it('keeps gdscript/zsh bullet-list fences as code', () => {
expect(isLikelyProseFence('gdscript', BULLET_2)).toBe(false)
expect(isLikelyProseFence('zsh', BULLET_2)).toBe(false)
})

it('keeps NON_CODE-family bullet-list fences as code', () => {
expect(isLikelyProseFence('text', BULLET_2)).toBe(false)
expect(isLikelyProseFence('plain', BULLET_2)).toBe(false)
expect(isLikelyProseFence('plaintext', BULLET_2)).toBe(false)
})

// Stone441 macOS repro: text/prompt fences with numbered Chinese prose.
it('keeps text/prompt fences with numbered Chinese prose as code (Stone441)', () => {
expect(isLikelyProseFence('text', CHINESE_NUMBERED)).toBe(false)
expect(isLikelyProseFence('prompt', CHINESE_NUMBERED)).toBe(false)
})
})
41 changes: 39 additions & 2 deletions apps/desktop/src/lib/markdown-code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,16 @@ export function isLikelyProseFence(info: string, body: string): boolean {
return true
}

// Any explicit, valid language tag is respected as code. sanitizeLanguageTag
// only returns non-empty for tags matching VALID_LANGUAGE_RE, so this covers
// text/plain/plaintext/md/markdown (in NON_CODE_FENCE_LANGUAGES) as well as
// gdscript/zsh and other non-COMMON tags the fallback below would otherwise
// prose-classify. Bare fences ('') skip this guard and fall through to the
// structured/prose heuristics so untagged paragraph fences still unwrap.
if (language !== '' && VALID_LANGUAGE_RE.test(language)) {
return false
}

if (!NON_CODE_FENCE_LANGUAGES.has(language)) {
return false
}
Expand All @@ -386,10 +396,28 @@ export function isLikelyProseCodeBlock(language: string | undefined, code: strin
return false
}

// Any explicit, valid language tag is respected as code — even when the
// body is a bullet list (gdscript/zsh lists were prose-classified by the
// bullet heuristic below, which only exempts COMMON languages). Exception:
// Streamdown-mislabeled unknown tags like "heads" (bullet + inline
// markdown emphasis) stay prose per the upstream test locked in below.
if (
cleanLanguage !== '' &&
VALID_LANGUAGE_RE.test(cleanLanguage) &&
!(signals.bulletLines >= 1 && signals.hasMarkdown)
) {
return false
}

// A bullet list with markdown emphasis is prose even when it happens to be
// structured; the config veto below is only meant to protect config/kv
// listings, so let the bullet-prose case win first.
if (signals.bulletLines >= 1 && (signals.hasMarkdown || signals.proseLines >= 2)) {
// listings, so let the bullet-prose case win first. Known code languages
// (yaml/markdown) are exempt: their lists are data, not prose.
if (
signals.bulletLines >= 1 &&
(signals.hasMarkdown || signals.proseLines >= 2) &&
!COMMON_CODE_LANGUAGES.has(cleanLanguage)
) {
return true
}

Expand All @@ -399,6 +427,15 @@ export function isLikelyProseCodeBlock(language: string | undefined, code: strin
return false
}

// Any explicit, valid language tag is respected and rendered as code
// (covers gdscript/zsh and other non-COMMON languages, plus text/plain/
// plaintext/md/markdown). sanitizeLanguageTag returning non-empty means
// VALID_LANGUAGE_RE matched; only bare fences ('') and unknown tags fall
// through to the prose heuristic below.
if (cleanLanguage !== '' && VALID_LANGUAGE_RE.test(cleanLanguage)) {
return false
}

if (NON_CODE_FENCE_LANGUAGES.has(cleanLanguage)) {
return signals.proseLines >= 3 && signals.codeSignals === 0
}
Expand Down