diff --git a/apps/desktop/src/lib/markdown-code.test.ts b/apps/desktop/src/lib/markdown-code.test.ts index ec12187f2c31a..339c96e3b9dcc 100644 --- a/apps/desktop/src/lib/markdown-code.test.ts +++ b/apps/desktop/src/lib/markdown-code.test.ts @@ -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( @@ -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', () => { @@ -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) + }) }) diff --git a/apps/desktop/src/lib/markdown-code.ts b/apps/desktop/src/lib/markdown-code.ts index ccad28422873d..9afe0f388ab43 100644 --- a/apps/desktop/src/lib/markdown-code.ts +++ b/apps/desktop/src/lib/markdown-code.ts @@ -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 } @@ -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 } @@ -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 }