diff --git a/apps/desktop/src/lib/chat-messages/parts.test.ts b/apps/desktop/src/lib/chat-messages/parts.test.ts index f6d5bd4f64..0a64a8cfd3 100644 --- a/apps/desktop/src/lib/chat-messages/parts.test.ts +++ b/apps/desktop/src/lib/chat-messages/parts.test.ts @@ -1,75 +1,33 @@ +// Regression for #96657: an unquoted MEDIA: path with interior spaces rendered +// a card for the text before the first space and left the rest as prose. import { describe, expect, it } from 'vitest' -import { renderMediaTags } from './parts' +import { appendAssistantTextPart, chatMessageText, mediaTagValues, renderMediaTags } from './parts' -describe('renderMediaTags', () => { - it('renders a simple unquoted path without spaces', () => { - const result = renderMediaTags('MEDIA:/home/user/report.pdf') - expect(result).toContain('report.pdf') - expect(result).not.toContain('MEDIA:') - }) +const SPACED = '/home/hermes/Morten - Nobly Kickoff - Opening and cue cards EN.docx' +const CARD = `[File: Morten - Nobly Kickoff - Opening and cue cards EN.docx](#media:${encodeURIComponent(SPACED)})` - it('renders a quoted path with spaces', () => { - const result = renderMediaTags('MEDIA:"/home/user/My Document.docx"') - expect(result).toContain('My Document.docx') - expect(result).not.toContain('MEDIA:') - }) - - it('renders an unquoted path with interior spaces (#96657)', () => { - const input = 'MEDIA:/home/hermes/Morten - Nobly Kickoff - Opening and cue cards EN.docx' - const result = renderMediaTags(input) - // The full path must be captured — not truncated at the first space - expect(result).toContain('Morten - Nobly Kickoff - Opening and cue cards EN.docx') - expect(result).not.toContain('MEDIA:') - // The entire input should be consumed (no leftover literal text) - expect(result).toBe(renderMediaTags(input)) - }) - - it('renders a backtick-quoted path with spaces', () => { - const result = renderMediaTags('MEDIA:`/home/user/My File.png`') - expect(result).toContain('My File.png') - expect(result).not.toContain('MEDIA:') - }) - - it('renders a single-quote path with spaces', () => { - const result = renderMediaTags("MEDIA:'/home/user/My Audio.mp3'") - expect(result).toContain('My Audio.mp3') - expect(result).not.toContain('MEDIA:') - }) - - it('handles a MEDIA tag on its own line', () => { - const text = `Some text\nMEDIA:/home/user/Report Final.pdf\nMore text` - const result = renderMediaTags(text) - expect(result).toContain('Report Final.pdf') - expect(result).toContain('Some text') - expect(result).toContain('More text') - expect(result).not.toContain('MEDIA:') - }) - - it('handles multiple MEDIA tags on separate lines', () => { - const text = 'MEDIA:/home/user/First File.pdf\nMEDIA:/home/user/Second Image.png' - const result = renderMediaTags(text) - expect(result).toContain('First File.pdf') - expect(result).toContain('Second Image.png') - expect(result).not.toContain('MEDIA:') - }) - - it('renders a Windows path with spaces', () => { - const result = renderMediaTags( - 'MEDIA:C:\\Users\\Morten\\My Report.docx' +describe('renderMediaTags with interior spaces', () => { + it('keeps the whole spaced path in one card on every surface that reads MEDIA tags', () => { + expect(renderMediaTags(`MEDIA:${SPACED}`)).toBe(CARD) + expect(renderMediaTags(`Here you go: MEDIA:${SPACED} — enjoy`)).toBe(`Here you go: ${CARD} — enjoy`) + expect(renderMediaTags('MEDIA:C:\\Users\\Morten\\My Report.docx')).toBe( + '[File: My Report.docx](#media:C%3A%5CUsers%5CMorten%5CMy%20Report.docx)' ) - expect(result).toContain('My Report.docx') - expect(result).not.toContain('MEDIA:') + expect(mediaTagValues(`ready\nMEDIA:${SPACED}\nMEDIA:/tmp/a.png`)).toEqual([SPACED, '/tmp/a.png']) }) - it('renders a ~/-relative path with spaces', () => { - const result = renderMediaTags('MEDIA:~/Documents/My Notes.md') - expect(result).toContain('My Notes.md') - expect(result).not.toContain('MEDIA:') - }) + it('settles on the complete path when the stream splits inside it', () => { + const chunks = ['ready\nMEDIA:/tmp/AI', ' Brain/re', 'port.pdf', '\nall done'] + let parts = appendAssistantTextPart([], chunks[0]) - it('leaves non-MEDIA text intact', () => { - const result = renderMediaTags('Just some regular text without any media tags.') - expect(result).toBe('Just some regular text without any media tags.') + // Mid-stream the truncated prefix may render as a card; the next delta must undo it. + for (const chunk of chunks.slice(1)) { + parts = appendAssistantTextPart(parts, chunk) + } + + expect(chatMessageText({ id: 'a', parts, role: 'assistant' })).toBe( + 'ready\n[File: report.pdf](#media:%2Ftmp%2FAI%20Brain%2Freport.pdf)\nall done' + ) }) -}) \ No newline at end of file +})