import { describe, expect, it } from 'vitest' import type { NativeChatMessage } from './native-chat-types' import { normalizeImageTranscriptMessages } from './native-chat-image-transcript-markers' function userText(id: string, text: string): NativeChatMessage { return { id, role: 'user', blocks: [{ type: 'text', text }], timestamp: 1, source: 'transcript' } } describe('normalizeImageTranscriptMessages', () => { it('merges the paired [Image: source]/[Image #1] turns into one image-ref turn', () => { const out = normalizeImageTranscriptMessages([ userText('a', '[Image: source: /tmp/orca-paste-1-2.png]'), userText('b', '[Image #1] describe this') ]) expect(out).toHaveLength(1) expect(out[0]!.blocks).toEqual([ { type: 'image-ref', path: '/tmp/orca-paste-1-2.png' }, { type: 'text', text: 'describe this' } ]) }) it('converts a lone [Image: source] turn (no prompt) into an image-ref instead of raw text', () => { const out = normalizeImageTranscriptMessages([ userText('a', '[Image: source: /Users/me/Pictures/hero-image-2.png]') ]) expect(out).toHaveLength(1) expect(out[0]!.blocks).toEqual([ { type: 'image-ref', path: '/Users/me/Pictures/hero-image-2.png' } ]) }) it('folds every source and strips every prompt marker for a multi-image send', () => { const out = normalizeImageTranscriptMessages([ userText('a', '[Image: source: /tmp/a.png]'), userText('b', '[Image: source: /tmp/b.png]'), userText('c', '[Image: source: /tmp/c.png]'), userText('prompt', '[Image #1] [Image #2] [Image #3] compare these') ]) expect(out).toHaveLength(1) expect(out[0]).toMatchObject({ id: 'prompt' }) expect(out[0]!.blocks).toEqual([ { type: 'image-ref', path: '/tmp/a.png' }, { type: 'image-ref', path: '/tmp/b.png' }, { type: 'image-ref', path: '/tmp/c.png' }, { type: 'text', text: 'compare these' } ]) }) it('keeps all image refs when a multi-image send has no caption', () => { const out = normalizeImageTranscriptMessages([ userText('a', '[Image: source: /tmp/a.png]'), userText('b', '[Image: source: /tmp/b.png]'), userText('prompt', '[Image #1] [Image #2]') ]) expect(out).toHaveLength(1) expect(out[0]!.blocks).toEqual([ { type: 'image-ref', path: '/tmp/a.png' }, { type: 'image-ref', path: '/tmp/b.png' } ]) }) it('preserves adjacent standalone image turns without a prompt marker', () => { const out = normalizeImageTranscriptMessages([ userText('a', '[Image: source: /tmp/a.png]'), userText('b', '[Image: source: /tmp/b.png]') ]) expect(out).toHaveLength(2) expect(out.map((message) => message.id)).toEqual(['a', 'b']) expect(out.map((message) => message.blocks)).toEqual([ [{ type: 'image-ref', path: '/tmp/a.png' }], [{ type: 'image-ref', path: '/tmp/b.png' }] ]) }) it('leaves ordinary user text untouched', () => { const message = userText('a', 'how about this') const messages = [message] const out = normalizeImageTranscriptMessages(messages) expect(out).toBe(messages) expect(out[0]).toBe(message) expect(out[0]!.blocks).toBe(message.blocks) }) it('removes a whitespace-only first text block', () => { const out = normalizeImageTranscriptMessages([userText('a', ' ')]) expect(out[0]?.blocks).toEqual([]) }) it('preserves unaffected rows when another row needs normalization', () => { const before = userText('before', 'keep this row') const marker = userText('marker', '[Image: source: /tmp/image.png]') const after = userText('after', 'keep this row too') const messages = [before, marker, after] const out = normalizeImageTranscriptMessages(messages) expect(out).not.toBe(messages) expect(out[0]).toBe(before) expect(out[2]).toBe(after) }) it('leaves assistant messages untouched', () => { const assistant: NativeChatMessage = { id: 'a', role: 'assistant', blocks: [{ type: 'text', text: '[Image: source: /tmp/x.png]' }], timestamp: 1, source: 'transcript' } expect(normalizeImageTranscriptMessages([assistant])).toEqual([assistant]) }) })