import { describe, expect, it } from 'vitest'; import { findRedoUserMessage, findUndoableUserMessage, getRestorableUserText, getStoredAttachmentsForRestore, } from '@/pages/Chat/chat-command-message'; import { normalizeOpencodeSessionMessages } from '@/lib/opencode-message'; import type { RawMessage } from '@/types/chat'; const messages: RawMessage[] = [ { id: 'msg_user_1', role: 'user', content: [ { type: 'text', text: 'Restore this text' }, { type: 'text', text: 'Synthetic path context', synthetic: true }, { id: 'part_image', type: 'image', mimeType: 'image/png', url: 'data:image/png;base64,QUFB', }, ], _attachedFiles: [ { fileName: 'screen.png', mimeType: 'image/png', fileSize: 3, preview: null, source: 'message-ref', }, { fileName: 'notes.md', mimeType: 'text/markdown', fileSize: 0, preview: null, filePath: 'D:/repo/notes.md', source: 'message-ref', }, ], }, { id: 'msg_assistant_1', role: 'assistant', content: [{ type: 'text', text: 'Done' }], }, { id: 'msg_user_2', role: 'user', content: [{ type: 'text', text: 'Second request' }], }, ]; describe('chat command message helpers', () => { it('restores only non-synthetic text', () => { expect(getRestorableUserText(messages[0])).toBe('Restore this text'); }); it('finds undo before the revert cursor and fails closed when the cursor is absent', () => { expect(findUndoableUserMessage(messages, undefined)?.id).toBe('msg_user_2'); expect(findUndoableUserMessage(messages, 'msg_user_2')?.id).toBe('msg_user_1'); expect(findUndoableUserMessage(messages, 'missing')).toBeNull(); expect(findUndoableUserMessage( [messages[1], { id: 'msg_system', role: 'system', content: 'notice' }], undefined, )).toBeNull(); }); it('restores a normalized safe command invocation rather than hidden template text', () => { const commandMessage: RawMessage = { id: 'msg_command', role: 'user', content: [ { type: 'text', text: '/Review raw arguments ' }, { type: 'text', text: 'Bounded selected path context', synthetic: true }, ], }; expect(findUndoableUserMessage([commandMessage], undefined)?.id).toBe('msg_command'); expect(getRestorableUserText(commandMessage)).toBe('/Review raw arguments'); }); it('finds the next user for redo and returns null when unrevert is next', () => { expect(findRedoUserMessage(messages, 'msg_user_1')?.id).toBe('msg_user_2'); expect(findRedoUserMessage(messages, 'msg_user_2')).toBeNull(); expect(findRedoUserMessage(messages, 'missing')).toBeNull(); }); it('pairs stored images with a validated data url and leaves path files path-only', () => { expect(getStoredAttachmentsForRestore(messages[0])).toEqual([ { file: messages[0]._attachedFiles?.[0], imageDataUrl: 'data:image/png;base64,QUFB', }, { file: messages[0]._attachedFiles?.[1], }, ]); }); it('never treats a remote image url as restorable bytes', () => { const remote: RawMessage = { id: 'msg_remote', role: 'user', content: [{ type: 'image', mimeType: 'image/png', url: 'https://runtime.invalid/private.png', }], _attachedFiles: [{ fileName: 'private.png', mimeType: 'image/png', fileSize: 0, preview: null, }], }; expect(getStoredAttachmentsForRestore(remote)).toEqual([ { file: remote._attachedFiles?.[0] }, ]); }); it('preserves remote and embedded image positions when MIME types repeat', () => { const interleaved: RawMessage = { id: 'msg_interleaved', role: 'user', content: [ { id: 'part_remote', type: 'image', mimeType: 'image/png', url: 'https://runtime.invalid/remote.png', }, { id: 'part_embedded', type: 'image', mimeType: 'image/png', url: 'data:image/png;base64,QUFB', }, ], _attachedFiles: [ { fileName: 'remote.png', mimeType: 'image/png', fileSize: 0, preview: null, runtimeUrl: 'https://runtime.invalid/remote.png', source: 'runtime-media', }, { fileName: 'embedded.png', mimeType: 'image/png', fileSize: 3, preview: null, source: 'message-ref', }, ], }; expect(getStoredAttachmentsForRestore(interleaved)).toEqual([ { file: interleaved._attachedFiles?.[0] }, { file: interleaved._attachedFiles?.[1], imageDataUrl: 'data:image/png;base64,QUFB', }, ]); }); it('consumes the preview-backed image position before pairing the next embedded image', () => { const previewThenEmbedded: RawMessage = { id: 'msg_preview_then_embedded', role: 'user', content: [ { type: 'image', mimeType: 'image/png', url: 'https://runtime.invalid/preview-backed.png', }, { type: 'image', mimeType: 'image/png', url: 'data:image/png;base64,QkJC', }, ], _attachedFiles: [ { fileName: 'preview-backed.png', mimeType: 'image/png', fileSize: 3, preview: 'data:image/png;base64,QUFB', }, { fileName: 'embedded.png', mimeType: 'image/png', fileSize: 3, preview: null, }, ], }; expect(getStoredAttachmentsForRestore(previewThenEmbedded)).toEqual([ { file: previewThenEmbedded._attachedFiles?.[0], imageDataUrl: 'data:image/png;base64,QUFB', }, { file: previewThenEmbedded._attachedFiles?.[1], imageDataUrl: 'data:image/png;base64,QkJC', }, ]); }); it('rejects empty base64 payloads and MIME-conflicting data urls', () => { const unsafe: RawMessage = { id: 'msg_unsafe_images', role: 'user', content: [ { type: 'image', mimeType: 'image/png', url: 'data:image/png;base64, ', }, { type: 'image', mimeType: 'image/png', url: 'data:image/jpeg;base64,QUFB', }, ], _attachedFiles: [ { fileName: 'empty.png', mimeType: 'image/png', fileSize: 0, preview: null, }, { fileName: 'mismatch.png', mimeType: 'image/png', fileSize: 3, preview: null, }, ], }; expect(getStoredAttachmentsForRestore(unsafe)).toEqual([ { file: unsafe._attachedFiles?.[0] }, { file: unsafe._attachedFiles?.[1] }, ]); }); it('preserves duplicate same-name image parts and restores their bytes in order', () => { const [normalized] = normalizeOpencodeSessionMessages([{ info: { id: 'msg_duplicate_images', role: 'user', time: { created: 1_700_000_004_000 }, }, parts: [ { id: 'prt_duplicate_invocation', type: 'command-invocation', command: 'Review', arguments: '', contextPartIDs: ['prt_duplicate_image_a', 'prt_duplicate_image_b'], }, { id: 'prt_duplicate_image_a', type: 'file', mime: 'image/png', filename: 'screen.png', url: 'data:image/png;base64,QUFB', }, { id: 'prt_duplicate_image_b', type: 'file', mime: 'image/png', filename: 'screen.png', url: 'data:image/png;base64,QkJC', }, ], }]); expect(normalized?.content).toEqual([ { id: 'prt_duplicate_invocation', type: 'text', text: '/Review', }, expect.objectContaining({ id: 'prt_duplicate_image_a', type: 'image', url: 'data:image/png;base64,QUFB', }), expect.objectContaining({ id: 'prt_duplicate_image_b', type: 'image', url: 'data:image/png;base64,QkJC', }), ]); expect(normalized?._attachedFiles).toHaveLength(2); expect(getStoredAttachmentsForRestore(normalized!)).toEqual([ { file: normalized?._attachedFiles?.[0], imageDataUrl: 'data:image/png;base64,QUFB', }, { file: normalized?._attachedFiles?.[1], imageDataUrl: 'data:image/png;base64,QkJC', }, ]); }); it('treats image MIME prefixes case-insensitively when restoring bytes', () => { const uppercaseMime: RawMessage = { id: 'msg_uppercase_mime', role: 'user', content: [{ id: 'part_uppercase_mime', type: 'image', mimeType: 'IMAGE/PNG', url: 'data:image/png;base64,QUFB', }], _attachedFiles: [{ fileName: 'upper.png', mimeType: 'IMAGE/PNG', fileSize: 3, preview: null, }], }; expect(getStoredAttachmentsForRestore(uppercaseMime)).toEqual([{ file: uppercaseMime._attachedFiles?.[0], imageDataUrl: 'data:image/png;base64,QUFB', }]); }); });