Files
makelore/tests/unit/chat-command-message.test.ts
2026-07-29 17:22:35 +08:00

336 lines
9.2 KiB
TypeScript

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',
}]);
});
});