mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-18 10:31:15 +00:00
2013 lines
53 KiB
TypeScript
2013 lines
53 KiB
TypeScript
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
import { useBuilderMessages } from '@/composables/useBuilderMessages';
|
|
import type { ChatUI } from '@n8n/design-system/types/assistant';
|
|
import type { ChatRequest } from '@/types/assistant.types';
|
|
|
|
// Mock useI18n to return the keys instead of translations
|
|
vi.mock('@n8n/i18n', () => ({
|
|
useI18n: () => ({
|
|
baseText: (key: string) => key,
|
|
}),
|
|
}));
|
|
|
|
describe('useBuilderMessages', () => {
|
|
let builderMessages: ReturnType<typeof useBuilderMessages>;
|
|
|
|
beforeEach(() => {
|
|
builderMessages = useBuilderMessages();
|
|
});
|
|
|
|
describe('processAssistantMessages', () => {
|
|
it('should process text messages correctly', () => {
|
|
const currentMessages: ChatUI.AssistantMessage[] = [];
|
|
const newMessages: ChatRequest.MessageResponse[] = [
|
|
{
|
|
type: 'message',
|
|
role: 'assistant',
|
|
text: 'Hello, how can I help?',
|
|
},
|
|
];
|
|
|
|
const result = builderMessages.processAssistantMessages(
|
|
currentMessages,
|
|
newMessages,
|
|
'test-id',
|
|
);
|
|
|
|
expect(result.messages).toHaveLength(1);
|
|
expect(result.messages[0]).toMatchObject({
|
|
id: 'test-id-0',
|
|
role: 'assistant',
|
|
type: 'text',
|
|
content: 'Hello, how can I help?',
|
|
read: false,
|
|
});
|
|
expect(result.shouldClearThinking).toBe(true);
|
|
});
|
|
|
|
it('should process tool messages with input data', () => {
|
|
const currentMessages: ChatUI.AssistantMessage[] = [];
|
|
const newMessages: ChatRequest.MessageResponse[] = [
|
|
{
|
|
role: 'assistant',
|
|
type: 'tool',
|
|
toolName: 'add_nodes',
|
|
toolCallId: 'call-123',
|
|
status: 'running',
|
|
updates: [
|
|
{
|
|
type: 'input',
|
|
data: { nodes: [{ name: 'HTTP Request' }] },
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
const result = builderMessages.processAssistantMessages(
|
|
currentMessages,
|
|
newMessages,
|
|
'test-id',
|
|
);
|
|
|
|
expect(result.messages).toHaveLength(1);
|
|
const toolMessage = result.messages[0] as ChatUI.ToolMessage;
|
|
expect(toolMessage).toMatchObject({
|
|
id: 'call-123', // Should use toolCallId as ID
|
|
role: 'assistant',
|
|
type: 'tool',
|
|
toolName: 'add_nodes',
|
|
toolCallId: 'call-123',
|
|
status: 'running',
|
|
read: false,
|
|
});
|
|
expect(toolMessage.updates).toHaveLength(1);
|
|
expect(toolMessage.updates[0]).toMatchObject({
|
|
type: 'input',
|
|
data: { nodes: [{ name: 'HTTP Request' }] },
|
|
});
|
|
});
|
|
|
|
it('should merge tool message updates when receiving output', () => {
|
|
// Start with a tool message that has input
|
|
const currentMessages: ChatUI.AssistantMessage[] = [
|
|
{
|
|
id: 'call-123',
|
|
role: 'assistant',
|
|
type: 'tool',
|
|
toolName: 'add_nodes',
|
|
toolCallId: 'call-123',
|
|
status: 'running',
|
|
updates: [
|
|
{
|
|
type: 'input',
|
|
data: { nodes: [{ name: 'HTTP Request' }] },
|
|
},
|
|
],
|
|
read: false,
|
|
} as ChatUI.AssistantMessage,
|
|
];
|
|
|
|
// Receive the output update
|
|
const newMessages: ChatRequest.MessageResponse[] = [
|
|
{
|
|
role: 'assistant',
|
|
type: 'tool',
|
|
toolName: 'add_nodes',
|
|
toolCallId: 'call-123',
|
|
status: 'completed',
|
|
updates: [
|
|
{
|
|
type: 'output',
|
|
data: { success: true, nodeId: 'node-1' },
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
const result = builderMessages.processAssistantMessages(
|
|
currentMessages,
|
|
newMessages,
|
|
'test-id',
|
|
);
|
|
|
|
expect(result.messages).toHaveLength(1);
|
|
const toolMessage = result.messages[0] as ChatUI.ToolMessage;
|
|
expect(toolMessage.status).toBe('completed');
|
|
expect(toolMessage.updates).toHaveLength(2);
|
|
expect(toolMessage.updates[0]).toMatchObject({
|
|
type: 'input',
|
|
data: { nodes: [{ name: 'HTTP Request' }] },
|
|
});
|
|
expect(toolMessage.updates[1]).toMatchObject({
|
|
type: 'output',
|
|
data: { success: true, nodeId: 'node-1' },
|
|
});
|
|
});
|
|
|
|
it('should handle tool messages without toolCallId', () => {
|
|
const currentMessages: ChatUI.AssistantMessage[] = [];
|
|
const newMessages: ChatRequest.MessageResponse[] = [
|
|
{
|
|
role: 'assistant',
|
|
type: 'tool',
|
|
toolName: 'some_tool',
|
|
status: 'completed',
|
|
updates: [],
|
|
},
|
|
];
|
|
|
|
const result = builderMessages.processAssistantMessages(
|
|
currentMessages,
|
|
newMessages,
|
|
'test-id',
|
|
);
|
|
|
|
expect(result.messages).toHaveLength(1);
|
|
expect(result.messages[0]).toMatchObject({
|
|
id: 'test-id-0', // Should fall back to generated ID
|
|
type: 'tool',
|
|
toolName: 'some_tool',
|
|
});
|
|
});
|
|
|
|
it('should not merge updates for different tool calls', () => {
|
|
const currentMessages: ChatUI.AssistantMessage[] = [
|
|
{
|
|
id: 'call-123',
|
|
role: 'assistant',
|
|
type: 'tool',
|
|
toolName: 'add_nodes',
|
|
toolCallId: 'call-123',
|
|
status: 'completed',
|
|
updates: [{ type: 'input', data: { test: 1 } }],
|
|
read: false,
|
|
} as ChatUI.AssistantMessage,
|
|
];
|
|
|
|
const newMessages: ChatRequest.MessageResponse[] = [
|
|
{
|
|
role: 'assistant',
|
|
type: 'tool',
|
|
toolName: 'add_nodes',
|
|
toolCallId: 'call-456', // Different ID
|
|
status: 'running',
|
|
updates: [{ type: 'input', data: { test: 2 } }],
|
|
},
|
|
];
|
|
|
|
const result = builderMessages.processAssistantMessages(
|
|
currentMessages,
|
|
newMessages,
|
|
'test-id',
|
|
);
|
|
|
|
expect(result.messages).toHaveLength(2);
|
|
expect((result.messages[0] as ChatUI.ToolMessage).toolCallId).toBe('call-123');
|
|
expect((result.messages[1] as ChatUI.ToolMessage).toolCallId).toBe('call-456');
|
|
});
|
|
|
|
it('should handle workflow updated messages', () => {
|
|
const currentMessages: ChatUI.AssistantMessage[] = [];
|
|
const newMessages: ChatRequest.MessageResponse[] = [
|
|
{
|
|
role: 'assistant',
|
|
type: 'workflow-updated',
|
|
codeSnippet: '{"nodes": [], "connections": {}}',
|
|
},
|
|
];
|
|
|
|
const result = builderMessages.processAssistantMessages(
|
|
currentMessages,
|
|
newMessages,
|
|
'test-id',
|
|
);
|
|
|
|
expect(result.messages).toHaveLength(1);
|
|
expect(result.messages[0]).toMatchObject({
|
|
id: 'test-id-0',
|
|
type: 'workflow-updated',
|
|
codeSnippet: '{"nodes": [], "connections": {}}',
|
|
read: false,
|
|
});
|
|
});
|
|
|
|
it('should handle mixed message types in a single batch', () => {
|
|
const currentMessages: ChatUI.AssistantMessage[] = [];
|
|
const newMessages: ChatRequest.MessageResponse[] = [
|
|
{
|
|
type: 'message',
|
|
role: 'assistant',
|
|
text: 'Creating workflow...',
|
|
},
|
|
{
|
|
role: 'assistant',
|
|
type: 'tool',
|
|
toolName: 'add_nodes',
|
|
toolCallId: 'call-123',
|
|
status: 'running',
|
|
updates: [{ type: 'input', data: { nodes: [] } }],
|
|
},
|
|
{
|
|
role: 'assistant',
|
|
type: 'workflow-updated',
|
|
codeSnippet: '{}',
|
|
},
|
|
];
|
|
|
|
const result = builderMessages.processAssistantMessages(
|
|
currentMessages,
|
|
newMessages,
|
|
'batch-id',
|
|
);
|
|
|
|
expect(result.messages).toHaveLength(3);
|
|
expect(result.messages[0].type).toBe('text');
|
|
expect(result.messages[0].id).toBe('batch-id-0');
|
|
expect(result.messages[1].type).toBe('tool');
|
|
expect(result.messages[1].id).toBe('call-123'); // Uses toolCallId
|
|
expect(result.messages[2].type).toBe('workflow-updated');
|
|
expect(result.messages[2].id).toBe('batch-id-2');
|
|
});
|
|
|
|
it('should show running tools message when tools are in progress', () => {
|
|
const currentMessages: ChatUI.AssistantMessage[] = [];
|
|
const newMessages: ChatRequest.MessageResponse[] = [
|
|
{
|
|
role: 'assistant',
|
|
type: 'tool',
|
|
toolName: 'add_nodes',
|
|
toolCallId: 'call-123',
|
|
status: 'running',
|
|
updates: [{ type: 'input', data: { nodes: [] } }],
|
|
},
|
|
];
|
|
|
|
const result = builderMessages.processAssistantMessages(
|
|
currentMessages,
|
|
newMessages,
|
|
'test-id',
|
|
);
|
|
|
|
expect(result.messages).toHaveLength(1);
|
|
expect(result.thinkingMessage).toBe('aiAssistant.thinkingSteps.runningTools');
|
|
expect(result.shouldClearThinking).toBe(false);
|
|
});
|
|
|
|
it('should show processing message when tools are completed but no text response yet', () => {
|
|
const currentMessages: ChatUI.AssistantMessage[] = [];
|
|
const newMessages: ChatRequest.MessageResponse[] = [
|
|
{
|
|
role: 'assistant',
|
|
type: 'tool',
|
|
toolName: 'add_nodes',
|
|
toolCallId: 'call-123',
|
|
status: 'completed',
|
|
updates: [
|
|
{ type: 'input', data: { nodes: [] } },
|
|
{ type: 'output', data: { success: true } },
|
|
],
|
|
},
|
|
];
|
|
|
|
const result = builderMessages.processAssistantMessages(
|
|
currentMessages,
|
|
newMessages,
|
|
'test-id',
|
|
);
|
|
|
|
expect(result.messages).toHaveLength(1);
|
|
expect(result.thinkingMessage).toBe('aiAssistant.thinkingSteps.processingResults');
|
|
expect(result.shouldClearThinking).toBe(false);
|
|
});
|
|
|
|
it('should not show thinking message when there is a text response after tools', () => {
|
|
const currentMessages: ChatUI.AssistantMessage[] = [
|
|
{
|
|
id: 'call-123',
|
|
role: 'assistant',
|
|
type: 'tool',
|
|
toolName: 'add_nodes',
|
|
toolCallId: 'call-123',
|
|
status: 'completed',
|
|
updates: [],
|
|
read: false,
|
|
} as ChatUI.AssistantMessage,
|
|
];
|
|
const newMessages: ChatRequest.MessageResponse[] = [
|
|
{
|
|
type: 'message',
|
|
role: 'assistant',
|
|
text: 'I have added the nodes for you.',
|
|
},
|
|
];
|
|
|
|
const result = builderMessages.processAssistantMessages(
|
|
currentMessages,
|
|
newMessages,
|
|
'test-id',
|
|
);
|
|
|
|
expect(result.messages).toHaveLength(2);
|
|
expect(result.thinkingMessage).toBeUndefined();
|
|
expect(result.shouldClearThinking).toBe(true);
|
|
});
|
|
|
|
it('should show correct thinking message for sequential tools', () => {
|
|
// First tool completes
|
|
const currentMessages: ChatUI.AssistantMessage[] = [
|
|
{
|
|
id: 'call-123',
|
|
role: 'assistant',
|
|
type: 'tool',
|
|
toolName: 'add_nodes',
|
|
toolCallId: 'call-123',
|
|
status: 'completed',
|
|
updates: [
|
|
{ type: 'input', data: { nodes: [] } },
|
|
{ type: 'output', data: { success: true } },
|
|
],
|
|
read: false,
|
|
} as ChatUI.AssistantMessage,
|
|
];
|
|
|
|
// Second tool starts running
|
|
const newMessages: ChatRequest.MessageResponse[] = [
|
|
{
|
|
role: 'assistant',
|
|
type: 'tool',
|
|
toolName: 'connect_nodes',
|
|
toolCallId: 'call-456',
|
|
status: 'running',
|
|
updates: [{ type: 'input', data: { from: 'node1', to: 'node2' } }],
|
|
},
|
|
];
|
|
|
|
const result = builderMessages.processAssistantMessages(
|
|
currentMessages,
|
|
newMessages,
|
|
'test-id',
|
|
);
|
|
|
|
expect(result.messages).toHaveLength(2);
|
|
// Should show "aiAssistant.thinkingSteps.runningTools" for the new running tool, not "aiAssistant.thinkingSteps.processingResults"
|
|
expect(result.thinkingMessage).toBe('aiAssistant.thinkingSteps.runningTools');
|
|
});
|
|
|
|
it('should show processing message when second tool completes', () => {
|
|
// Both tools completed
|
|
const currentMessages: ChatUI.AssistantMessage[] = [
|
|
{
|
|
id: 'call-123',
|
|
role: 'assistant',
|
|
type: 'tool',
|
|
toolName: 'add_nodes',
|
|
toolCallId: 'call-123',
|
|
status: 'completed',
|
|
updates: [],
|
|
read: false,
|
|
} as ChatUI.AssistantMessage,
|
|
];
|
|
|
|
const newMessages: ChatRequest.MessageResponse[] = [
|
|
{
|
|
role: 'assistant',
|
|
type: 'tool',
|
|
toolName: 'connect_nodes',
|
|
toolCallId: 'call-456',
|
|
status: 'completed',
|
|
updates: [],
|
|
},
|
|
];
|
|
|
|
const result = builderMessages.processAssistantMessages(
|
|
currentMessages,
|
|
newMessages,
|
|
'test-id',
|
|
);
|
|
|
|
expect(result.messages).toHaveLength(2);
|
|
expect(result.thinkingMessage).toBe('aiAssistant.thinkingSteps.processingResults');
|
|
});
|
|
|
|
it('should keep showing running tools message when parallel tools complete one by one', () => {
|
|
// Two tools running
|
|
const currentMessages: ChatUI.AssistantMessage[] = [
|
|
{
|
|
id: 'call-123',
|
|
role: 'assistant',
|
|
type: 'tool',
|
|
toolName: 'add_nodes',
|
|
toolCallId: 'call-123',
|
|
status: 'running',
|
|
updates: [],
|
|
read: false,
|
|
} as ChatUI.AssistantMessage,
|
|
{
|
|
id: 'call-456',
|
|
role: 'assistant',
|
|
type: 'tool',
|
|
toolName: 'connect_nodes',
|
|
toolCallId: 'call-456',
|
|
status: 'running',
|
|
updates: [],
|
|
read: false,
|
|
} as ChatUI.AssistantMessage,
|
|
];
|
|
|
|
// First tool completes
|
|
const newMessages: ChatRequest.MessageResponse[] = [
|
|
{
|
|
role: 'assistant',
|
|
type: 'tool',
|
|
toolName: 'add_nodes',
|
|
toolCallId: 'call-123',
|
|
status: 'completed',
|
|
updates: [{ type: 'output', data: { success: true } }],
|
|
},
|
|
];
|
|
|
|
const result = builderMessages.processAssistantMessages(
|
|
currentMessages,
|
|
newMessages,
|
|
'test-id',
|
|
);
|
|
|
|
expect(result.messages).toHaveLength(2);
|
|
// Should still show "aiAssistant.thinkingSteps.runningTools" because call-456 is still running
|
|
expect(result.thinkingMessage).toBe('aiAssistant.thinkingSteps.runningTools');
|
|
|
|
// Verify first tool is now completed
|
|
const firstTool = result.messages.find(
|
|
(m) => (m as ChatUI.ToolMessage).toolCallId === 'call-123',
|
|
) as ChatUI.ToolMessage;
|
|
expect(firstTool.status).toBe('completed');
|
|
|
|
// Verify second tool is still running
|
|
const secondTool = result.messages.find(
|
|
(m) => (m as ChatUI.ToolMessage).toolCallId === 'call-456',
|
|
) as ChatUI.ToolMessage;
|
|
expect(secondTool.status).toBe('running');
|
|
});
|
|
|
|
it('should show processing results when all parallel tools complete', () => {
|
|
// One tool already completed, one still running
|
|
const currentMessages: ChatUI.AssistantMessage[] = [
|
|
{
|
|
id: 'call-123',
|
|
role: 'assistant',
|
|
type: 'tool',
|
|
toolName: 'add_nodes',
|
|
toolCallId: 'call-123',
|
|
status: 'completed',
|
|
updates: [],
|
|
read: false,
|
|
} as ChatUI.AssistantMessage,
|
|
{
|
|
id: 'call-456',
|
|
role: 'assistant',
|
|
type: 'tool',
|
|
toolName: 'connect_nodes',
|
|
toolCallId: 'call-456',
|
|
status: 'running',
|
|
updates: [],
|
|
read: false,
|
|
} as ChatUI.AssistantMessage,
|
|
];
|
|
|
|
// Second tool completes
|
|
const newMessages: ChatRequest.MessageResponse[] = [
|
|
{
|
|
role: 'assistant',
|
|
type: 'tool',
|
|
toolName: 'connect_nodes',
|
|
toolCallId: 'call-456',
|
|
status: 'completed',
|
|
updates: [{ type: 'output', data: { success: true } }],
|
|
},
|
|
];
|
|
|
|
const result = builderMessages.processAssistantMessages(
|
|
currentMessages,
|
|
newMessages,
|
|
'test-id',
|
|
);
|
|
|
|
expect(result.messages).toHaveLength(2);
|
|
// Should now show "aiAssistant.thinkingSteps.processingResults" because all tools are completed
|
|
expect(result.thinkingMessage).toBe('aiAssistant.thinkingSteps.processingResults');
|
|
});
|
|
|
|
it('should keep processing message when workflow-updated arrives after tools complete', () => {
|
|
// Tool completed
|
|
const currentMessages: ChatUI.AssistantMessage[] = [
|
|
{
|
|
id: 'call-123',
|
|
role: 'assistant',
|
|
type: 'tool',
|
|
toolName: 'add_nodes',
|
|
toolCallId: 'call-123',
|
|
status: 'completed',
|
|
updates: [],
|
|
read: false,
|
|
} as ChatUI.AssistantMessage,
|
|
];
|
|
|
|
// Workflow update arrives
|
|
const newMessages: ChatRequest.MessageResponse[] = [
|
|
{
|
|
role: 'assistant',
|
|
type: 'workflow-updated',
|
|
codeSnippet: '{"nodes": [], "connections": {}}',
|
|
},
|
|
];
|
|
|
|
const result = builderMessages.processAssistantMessages(
|
|
currentMessages,
|
|
newMessages,
|
|
'test-id',
|
|
);
|
|
|
|
expect(result.messages).toHaveLength(2);
|
|
// Should still show "aiAssistant.thinkingSteps.processingResults" because workflow-updated is not a text response
|
|
expect(result.thinkingMessage).toBe('aiAssistant.thinkingSteps.processingResults');
|
|
// Should NOT clear thinking for workflow updates
|
|
expect(result.shouldClearThinking).toBe(false);
|
|
});
|
|
|
|
it('should clear processing message only when text arrives after workflow-updated', () => {
|
|
// Tool completed and workflow updated
|
|
const currentMessages: ChatUI.AssistantMessage[] = [
|
|
{
|
|
id: 'call-123',
|
|
role: 'assistant',
|
|
type: 'tool',
|
|
toolName: 'add_nodes',
|
|
toolCallId: 'call-123',
|
|
status: 'completed',
|
|
updates: [],
|
|
read: false,
|
|
} as ChatUI.AssistantMessage,
|
|
{
|
|
id: 'workflow-1',
|
|
role: 'assistant',
|
|
type: 'workflow-updated',
|
|
codeSnippet: '{"nodes": [], "connections": {}}',
|
|
read: false,
|
|
} as ChatUI.AssistantMessage,
|
|
];
|
|
|
|
// Text message arrives
|
|
const newMessages: ChatRequest.MessageResponse[] = [
|
|
{
|
|
type: 'message',
|
|
role: 'assistant',
|
|
text: 'I have created the workflow for you.',
|
|
},
|
|
];
|
|
|
|
const result = builderMessages.processAssistantMessages(
|
|
currentMessages,
|
|
newMessages,
|
|
'test-id',
|
|
);
|
|
|
|
expect(result.messages).toHaveLength(3);
|
|
// Should clear thinking message when text arrives
|
|
expect(result.thinkingMessage).toBeUndefined();
|
|
expect(result.shouldClearThinking).toBe(true);
|
|
});
|
|
|
|
it('should only apply rating to the last text message after workflow-updated', () => {
|
|
const currentMessages: ChatUI.AssistantMessage[] = [];
|
|
const newMessages: ChatRequest.MessageResponse[] = [
|
|
{
|
|
type: 'message',
|
|
role: 'assistant',
|
|
text: 'I will create a workflow for you.',
|
|
},
|
|
{
|
|
role: 'assistant',
|
|
type: 'workflow-updated',
|
|
codeSnippet: '{"nodes": [], "connections": {}}',
|
|
},
|
|
{
|
|
type: 'message',
|
|
role: 'assistant',
|
|
text: 'I have started creating the workflow.',
|
|
},
|
|
{
|
|
type: 'message',
|
|
role: 'assistant',
|
|
text: 'The workflow has been created successfully!',
|
|
},
|
|
];
|
|
|
|
const result = builderMessages.processAssistantMessages(
|
|
currentMessages,
|
|
newMessages,
|
|
'test-id',
|
|
);
|
|
|
|
expect(result.messages).toHaveLength(4);
|
|
|
|
// First text message should NOT have showRating (comes before workflow-updated)
|
|
const firstText = result.messages[0];
|
|
expect(firstText.showRating).toBeUndefined();
|
|
|
|
// Workflow-updated message should not have rating
|
|
expect(result.messages[1].type).toBe('workflow-updated');
|
|
|
|
// Middle text message should NOT have rating
|
|
const middleText = result.messages[2];
|
|
expect(middleText.showRating).toBeUndefined();
|
|
|
|
// Only the LAST text message should have showRating with regular style
|
|
const lastText = result.messages[3];
|
|
expect(lastText.showRating).toBe(true);
|
|
expect(lastText.ratingStyle).toBe('regular');
|
|
});
|
|
|
|
it('should not apply rating to text messages without workflow-updated', () => {
|
|
const currentMessages: ChatUI.AssistantMessage[] = [];
|
|
const newMessages: ChatRequest.MessageResponse[] = [
|
|
{
|
|
type: 'message',
|
|
role: 'assistant',
|
|
text: 'Hello, how can I help?',
|
|
},
|
|
];
|
|
|
|
const result = builderMessages.processAssistantMessages(
|
|
currentMessages,
|
|
newMessages,
|
|
'test-id',
|
|
);
|
|
|
|
expect(result.messages).toHaveLength(1);
|
|
const textMessage = result.messages[0];
|
|
expect(textMessage.showRating).toBeUndefined();
|
|
expect(textMessage.ratingStyle).toBeUndefined();
|
|
});
|
|
|
|
it('should not apply rating when tools are still running', () => {
|
|
const currentMessages: ChatUI.AssistantMessage[] = [];
|
|
const newMessages: ChatRequest.MessageResponse[] = [
|
|
{
|
|
type: 'message',
|
|
role: 'assistant',
|
|
text: 'Starting the process...',
|
|
},
|
|
{
|
|
role: 'assistant',
|
|
type: 'workflow-updated',
|
|
codeSnippet: '{}',
|
|
},
|
|
{
|
|
role: 'assistant',
|
|
type: 'tool',
|
|
toolName: 'add_nodes',
|
|
toolCallId: 'call-1',
|
|
status: 'running',
|
|
updates: [],
|
|
},
|
|
{
|
|
type: 'message',
|
|
role: 'assistant',
|
|
text: 'Still working on it...',
|
|
},
|
|
];
|
|
|
|
const result = builderMessages.processAssistantMessages(
|
|
currentMessages,
|
|
newMessages,
|
|
'test-id',
|
|
);
|
|
|
|
// No messages should have rating while tools are running
|
|
const firstMessage = result.messages[0];
|
|
expect(firstMessage.showRating).toBeUndefined();
|
|
|
|
const lastMessage = result.messages[3];
|
|
expect(lastMessage.showRating).toBeUndefined();
|
|
});
|
|
|
|
it('should apply rating to the last text message after all tools complete', () => {
|
|
const currentMessages: ChatUI.AssistantMessage[] = [
|
|
{
|
|
id: 'msg-1',
|
|
type: 'text',
|
|
role: 'assistant',
|
|
content: 'Starting the process...',
|
|
read: false,
|
|
},
|
|
{
|
|
id: 'workflow-1',
|
|
type: 'workflow-updated',
|
|
role: 'assistant',
|
|
codeSnippet: '{}',
|
|
read: false,
|
|
},
|
|
{
|
|
id: 'tool-1',
|
|
type: 'tool',
|
|
role: 'assistant',
|
|
toolName: 'add_nodes',
|
|
toolCallId: 'call-1',
|
|
status: 'completed',
|
|
updates: [],
|
|
read: false,
|
|
} as ChatUI.AssistantMessage,
|
|
{
|
|
id: 'msg-2',
|
|
type: 'text',
|
|
role: 'assistant',
|
|
content: 'All done!',
|
|
read: false,
|
|
},
|
|
];
|
|
|
|
const newMessages: ChatRequest.MessageResponse[] = [];
|
|
|
|
const result = builderMessages.processAssistantMessages(
|
|
currentMessages,
|
|
newMessages,
|
|
'test-id',
|
|
);
|
|
|
|
// Only the last text message should have rating
|
|
const firstMessage = result.messages[0];
|
|
expect(firstMessage.showRating).toBeUndefined();
|
|
|
|
const lastMessage = result.messages[3];
|
|
expect(lastMessage.showRating).toBe(true);
|
|
expect(lastMessage.ratingStyle).toBe('regular');
|
|
});
|
|
});
|
|
|
|
describe('mapAssistantMessageToUI', () => {
|
|
it('should map tool messages correctly for session loading', () => {
|
|
const message: ChatRequest.MessageResponse = {
|
|
role: 'assistant',
|
|
type: 'tool',
|
|
toolName: 'connect_nodes',
|
|
toolCallId: 'call-789',
|
|
status: 'completed',
|
|
updates: [
|
|
{ type: 'input', data: { from: 'node1', to: 'node2' } },
|
|
{ type: 'output', data: { success: true } },
|
|
],
|
|
};
|
|
|
|
const result = builderMessages.mapAssistantMessageToUI(message, 'session-msg-1');
|
|
|
|
expect(result).toMatchObject({
|
|
id: 'session-msg-1',
|
|
role: 'assistant',
|
|
type: 'tool',
|
|
toolName: 'connect_nodes',
|
|
toolCallId: 'call-789',
|
|
status: 'completed',
|
|
read: false,
|
|
});
|
|
expect((result as ChatUI.ToolMessage).updates).toHaveLength(2);
|
|
});
|
|
|
|
it('should handle tool messages without updates array', () => {
|
|
// @ts-expect-error: toolCallId is required, but not present in this test
|
|
const message: ChatRequest.MessageResponse = {
|
|
role: 'assistant',
|
|
type: 'tool',
|
|
toolName: 'some_tool',
|
|
toolCallId: 'call-999',
|
|
status: 'running',
|
|
};
|
|
|
|
const result = builderMessages.mapAssistantMessageToUI(message, 'test-id');
|
|
|
|
expect((result as ChatUI.ToolMessage).updates).toEqual([]);
|
|
});
|
|
|
|
it('should map text messages correctly', () => {
|
|
const message: ChatRequest.MessageResponse = {
|
|
type: 'message',
|
|
role: 'assistant',
|
|
text: 'Workflow created successfully!',
|
|
};
|
|
|
|
const result = builderMessages.mapAssistantMessageToUI(message, 'msg-id');
|
|
|
|
expect(result).toMatchObject({
|
|
id: 'msg-id',
|
|
role: 'assistant',
|
|
type: 'text',
|
|
content: 'Workflow created successfully!',
|
|
read: false,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('createUserMessage', () => {
|
|
it('should create a user message with correct properties', () => {
|
|
const message = builderMessages.createUserMessage('Help me build a workflow', 'user-msg-1');
|
|
|
|
expect(message).toMatchObject({
|
|
id: 'user-msg-1',
|
|
role: 'user',
|
|
type: 'text',
|
|
content: 'Help me build a workflow',
|
|
read: true,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('createErrorMessage', () => {
|
|
it('should create an error message without retry', () => {
|
|
const message = builderMessages.createErrorMessage('Something went wrong', 'error-1');
|
|
|
|
expect(message).toMatchObject({
|
|
id: 'error-1',
|
|
role: 'assistant',
|
|
type: 'error',
|
|
content: 'Something went wrong',
|
|
read: false,
|
|
});
|
|
expect((message as ChatUI.ErrorMessage).retry).toBeUndefined();
|
|
});
|
|
|
|
it('should create an error message with retry function', () => {
|
|
const retryFn = async () => {};
|
|
const message = builderMessages.createErrorMessage('Network error', 'error-2', retryFn);
|
|
|
|
expect(message).toMatchObject({
|
|
id: 'error-2',
|
|
role: 'assistant',
|
|
type: 'error',
|
|
content: 'Network error',
|
|
read: false,
|
|
});
|
|
expect((message as ChatUI.ErrorMessage).retry).toBe(retryFn);
|
|
});
|
|
|
|
it('should handle empty error message', () => {
|
|
const message = builderMessages.createErrorMessage('', 'error-empty');
|
|
|
|
expect(message).toMatchObject({
|
|
id: 'error-empty',
|
|
role: 'assistant',
|
|
type: 'error',
|
|
content: '',
|
|
read: false,
|
|
});
|
|
});
|
|
|
|
it('should handle very long error messages', () => {
|
|
const longMessage = 'Error: '.repeat(100);
|
|
const message = builderMessages.createErrorMessage(longMessage, 'error-long');
|
|
|
|
expect((message as ChatUI.ErrorMessage).content).toBe(longMessage);
|
|
expect(message.id).toBe('error-long');
|
|
});
|
|
});
|
|
|
|
describe('edge cases and malformed data', () => {
|
|
it('should handle empty message arrays', () => {
|
|
const result = builderMessages.processAssistantMessages([], [], 'test-id');
|
|
|
|
expect(result.messages).toHaveLength(0);
|
|
expect(result.shouldClearThinking).toBe(false);
|
|
expect(result.thinkingMessage).toBeUndefined();
|
|
});
|
|
|
|
it('should handle messages with missing required fields', () => {
|
|
const currentMessages: ChatUI.AssistantMessage[] = [];
|
|
const newMessages: ChatRequest.MessageResponse[] = [
|
|
{
|
|
role: 'assistant',
|
|
type: 'tool',
|
|
toolName: 'test_tool',
|
|
status: 'completed',
|
|
updates: [],
|
|
} as ChatRequest.MessageResponse,
|
|
];
|
|
|
|
const result = builderMessages.processAssistantMessages(
|
|
currentMessages,
|
|
newMessages,
|
|
'test-id',
|
|
);
|
|
|
|
expect(result.messages).toHaveLength(1);
|
|
expect(result.messages[0].id).toBe('test-id-0');
|
|
});
|
|
|
|
it('should handle workflow-updated messages with invalid JSON', () => {
|
|
const currentMessages: ChatUI.AssistantMessage[] = [];
|
|
const newMessages: ChatRequest.MessageResponse[] = [
|
|
{
|
|
role: 'assistant',
|
|
type: 'workflow-updated',
|
|
codeSnippet: 'invalid json {',
|
|
},
|
|
];
|
|
|
|
const result = builderMessages.processAssistantMessages(
|
|
currentMessages,
|
|
newMessages,
|
|
'test-id',
|
|
);
|
|
|
|
expect(result.messages).toHaveLength(1);
|
|
expect(result.messages[0]).toMatchObject({
|
|
type: 'workflow-updated',
|
|
codeSnippet: 'invalid json {',
|
|
});
|
|
});
|
|
|
|
it('should handle tool messages with corrupted update data', () => {
|
|
const currentMessages: ChatUI.AssistantMessage[] = [];
|
|
const newMessages: ChatRequest.MessageResponse[] = [
|
|
{
|
|
role: 'assistant',
|
|
type: 'tool',
|
|
toolName: 'add_nodes',
|
|
toolCallId: 'call-123',
|
|
status: 'completed',
|
|
updates: [
|
|
{
|
|
type: 'output',
|
|
// @ts-expect-error testing invalid data
|
|
data: null,
|
|
},
|
|
{
|
|
type: 'input',
|
|
// @ts-expect-error testing invalid data
|
|
data: undefined,
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
const result = builderMessages.processAssistantMessages(
|
|
currentMessages,
|
|
newMessages,
|
|
'test-id',
|
|
);
|
|
|
|
expect(result.messages).toHaveLength(1);
|
|
const toolMessage = result.messages[0] as ChatUI.ToolMessage;
|
|
expect(toolMessage.updates).toHaveLength(2);
|
|
expect(toolMessage.updates[0].data).toBeNull();
|
|
expect(toolMessage.updates[1].data).toBeUndefined();
|
|
});
|
|
|
|
it('should handle duplicate message IDs gracefully', () => {
|
|
const currentMessages: ChatUI.AssistantMessage[] = [
|
|
{
|
|
id: 'call-123',
|
|
role: 'assistant',
|
|
type: 'tool',
|
|
toolName: 'add_nodes',
|
|
toolCallId: 'call-123',
|
|
status: 'running',
|
|
updates: [],
|
|
read: false,
|
|
} as ChatUI.AssistantMessage,
|
|
];
|
|
|
|
const newMessages: ChatRequest.MessageResponse[] = [
|
|
{
|
|
role: 'assistant',
|
|
type: 'tool',
|
|
toolName: 'add_nodes',
|
|
toolCallId: 'call-123',
|
|
status: 'completed',
|
|
updates: [{ type: 'output', data: { success: true } }],
|
|
},
|
|
];
|
|
|
|
const result = builderMessages.processAssistantMessages(
|
|
currentMessages,
|
|
newMessages,
|
|
'test-id',
|
|
);
|
|
|
|
expect(result.messages).toHaveLength(1);
|
|
const toolMessage = result.messages[0] as ChatUI.ToolMessage;
|
|
expect(toolMessage.status).toBe('completed');
|
|
expect(toolMessage.updates).toHaveLength(1);
|
|
});
|
|
|
|
it('should handle messages with missing text content', () => {
|
|
const currentMessages: ChatUI.AssistantMessage[] = [];
|
|
const newMessages: ChatRequest.MessageResponse[] = [
|
|
{
|
|
type: 'message',
|
|
role: 'assistant',
|
|
// @ts-expect-error testing invalid data
|
|
text: undefined,
|
|
},
|
|
];
|
|
|
|
const result = builderMessages.processAssistantMessages(
|
|
currentMessages,
|
|
newMessages,
|
|
'test-id',
|
|
);
|
|
|
|
expect(result.messages).toHaveLength(1);
|
|
expect(result.messages[0]).toMatchObject({
|
|
type: 'text',
|
|
content: undefined,
|
|
});
|
|
});
|
|
|
|
it('should handle extremely large message batches', () => {
|
|
const currentMessages: ChatUI.AssistantMessage[] = [];
|
|
const newMessages: ChatRequest.MessageResponse[] = Array.from({ length: 1000 }, (_, i) => ({
|
|
type: 'message',
|
|
role: 'assistant',
|
|
text: `Message ${i}`,
|
|
}));
|
|
|
|
const result = builderMessages.processAssistantMessages(
|
|
currentMessages,
|
|
newMessages,
|
|
'test-id',
|
|
);
|
|
|
|
expect(result.messages).toHaveLength(1000);
|
|
expect((result.messages as ChatUI.TextMessage[])[0].content).toBe('Message 0');
|
|
expect((result.messages as ChatUI.TextMessage[])[999].content).toBe('Message 999');
|
|
});
|
|
});
|
|
|
|
describe('complex workflow scenarios', () => {
|
|
it('should handle multiple interleaved tool calls with workflow updates', () => {
|
|
const currentMessages: ChatUI.AssistantMessage[] = [];
|
|
const newMessages: ChatRequest.MessageResponse[] = [
|
|
{
|
|
role: 'assistant',
|
|
type: 'tool',
|
|
toolName: 'add_nodes',
|
|
toolCallId: 'call-1',
|
|
status: 'running',
|
|
updates: [{ type: 'input', data: { nodes: ['HTTP Request'] } }],
|
|
},
|
|
{
|
|
role: 'assistant',
|
|
type: 'tool',
|
|
toolName: 'connect_nodes',
|
|
toolCallId: 'call-2',
|
|
status: 'running',
|
|
updates: [{ type: 'input', data: { from: 'node1', to: 'node2' } }],
|
|
},
|
|
{
|
|
role: 'assistant',
|
|
type: 'workflow-updated',
|
|
codeSnippet: '{"nodes": [{"name": "HTTP Request"}], "connections": {}}',
|
|
},
|
|
{
|
|
role: 'assistant',
|
|
type: 'tool',
|
|
toolName: 'add_nodes',
|
|
toolCallId: 'call-1',
|
|
status: 'completed',
|
|
updates: [{ type: 'output', data: { success: true, nodeId: 'node1' } }],
|
|
},
|
|
{
|
|
role: 'assistant',
|
|
type: 'tool',
|
|
toolName: 'connect_nodes',
|
|
toolCallId: 'call-2',
|
|
status: 'completed',
|
|
updates: [{ type: 'output', data: { success: true, connectionId: 'conn1' } }],
|
|
},
|
|
{
|
|
role: 'assistant',
|
|
type: 'workflow-updated',
|
|
codeSnippet: '{"nodes": [{"name": "HTTP Request"}], "connections": {"conn1": {}}}',
|
|
},
|
|
{
|
|
type: 'message',
|
|
role: 'assistant',
|
|
text: 'I have successfully created and connected the nodes in your workflow.',
|
|
},
|
|
];
|
|
|
|
const result = builderMessages.processAssistantMessages(
|
|
currentMessages,
|
|
newMessages,
|
|
'test-id',
|
|
);
|
|
|
|
expect(result.messages).toHaveLength(5);
|
|
|
|
// Check tool message updates are merged correctly
|
|
const tool1 = result.messages.find(
|
|
(m) => (m as ChatUI.ToolMessage).toolCallId === 'call-1',
|
|
) as ChatUI.ToolMessage;
|
|
expect(tool1.updates).toHaveLength(2);
|
|
expect(tool1.status).toBe('completed');
|
|
|
|
const tool2 = result.messages.find(
|
|
(m) => (m as ChatUI.ToolMessage).toolCallId === 'call-2',
|
|
) as ChatUI.ToolMessage;
|
|
expect(tool2.updates).toHaveLength(2);
|
|
expect(tool2.status).toBe('completed');
|
|
|
|
// Check workflow updates are present
|
|
const workflowUpdates = result.messages.filter((m) => m.type === 'workflow-updated');
|
|
expect(workflowUpdates).toHaveLength(2);
|
|
|
|
// Check final text message has rating
|
|
const textMessage = result.messages.find((m) => m.type === 'text');
|
|
expect(textMessage?.showRating).toBe(true);
|
|
expect(textMessage?.ratingStyle).toBe('regular');
|
|
|
|
// Should clear thinking since tools are complete and text is present
|
|
expect(result.shouldClearThinking).toBe(true);
|
|
expect(result.thinkingMessage).toBeUndefined();
|
|
});
|
|
|
|
it('should handle failed tool calls and recovery', () => {
|
|
const currentMessages: ChatUI.AssistantMessage[] = [];
|
|
const newMessages: ChatRequest.MessageResponse[] = [
|
|
{
|
|
role: 'assistant',
|
|
type: 'tool',
|
|
toolName: 'add_nodes',
|
|
toolCallId: 'call-1',
|
|
status: 'running',
|
|
updates: [{ type: 'input', data: { nodes: ['Invalid Node'] } }],
|
|
},
|
|
{
|
|
role: 'assistant',
|
|
type: 'tool',
|
|
toolName: 'add_nodes',
|
|
toolCallId: 'call-1',
|
|
status: 'error',
|
|
updates: [{ type: 'output', data: { error: 'Node type not found' } }],
|
|
},
|
|
{
|
|
role: 'assistant',
|
|
type: 'tool',
|
|
toolName: 'add_nodes',
|
|
toolCallId: 'call-2',
|
|
status: 'running',
|
|
updates: [{ type: 'input', data: { nodes: ['HTTP Request'] } }],
|
|
},
|
|
{
|
|
role: 'assistant',
|
|
type: 'tool',
|
|
toolName: 'add_nodes',
|
|
toolCallId: 'call-2',
|
|
status: 'completed',
|
|
updates: [{ type: 'output', data: { success: true, nodeId: 'node1' } }],
|
|
},
|
|
{
|
|
role: 'assistant',
|
|
type: 'workflow-updated',
|
|
codeSnippet: '{"nodes": [{"name": "HTTP Request"}], "connections": {}}',
|
|
},
|
|
{
|
|
type: 'message',
|
|
role: 'assistant',
|
|
text: 'I encountered an error with the first node type, but successfully added an HTTP Request node instead.',
|
|
},
|
|
];
|
|
|
|
const result = builderMessages.processAssistantMessages(
|
|
currentMessages,
|
|
newMessages,
|
|
'test-id',
|
|
);
|
|
|
|
expect(result.messages).toHaveLength(4);
|
|
|
|
// Check failed tool
|
|
const failedTool = result.messages.find(
|
|
(m) => (m as ChatUI.ToolMessage).toolCallId === 'call-1',
|
|
) as ChatUI.ToolMessage;
|
|
expect(failedTool.status).toBe('error');
|
|
expect(failedTool.updates).toHaveLength(2);
|
|
|
|
// Check successful tool
|
|
const successTool = result.messages.find(
|
|
(m) => (m as ChatUI.ToolMessage).toolCallId === 'call-2',
|
|
) as ChatUI.ToolMessage;
|
|
expect(successTool.status).toBe('completed');
|
|
expect(successTool.updates).toHaveLength(2);
|
|
|
|
// Check workflow update and text message
|
|
const workflowUpdate = result.messages.find((m) => m.type === 'workflow-updated');
|
|
expect(workflowUpdate).toBeTruthy();
|
|
|
|
const textMessage = result.messages.find((m) => m.type === 'text');
|
|
expect(textMessage?.showRating).toBe(true);
|
|
expect(textMessage?.content).toContain('error');
|
|
});
|
|
|
|
it('should handle rapid tool status changes', () => {
|
|
let currentMessages: ChatUI.AssistantMessage[] = [];
|
|
|
|
// First batch: tool starts
|
|
const batch1: ChatRequest.MessageResponse[] = [
|
|
{
|
|
role: 'assistant',
|
|
type: 'tool',
|
|
toolName: 'add_nodes',
|
|
toolCallId: 'call-1',
|
|
status: 'running',
|
|
updates: [{ type: 'input', data: { nodes: ['HTTP Request'] } }],
|
|
},
|
|
];
|
|
|
|
let result = builderMessages.processAssistantMessages(currentMessages, batch1, 'batch-1');
|
|
expect(result.thinkingMessage).toBe('aiAssistant.thinkingSteps.runningTools');
|
|
currentMessages = result.messages;
|
|
|
|
// Second batch: tool completes
|
|
const batch2: ChatRequest.MessageResponse[] = [
|
|
{
|
|
role: 'assistant',
|
|
type: 'tool',
|
|
toolName: 'add_nodes',
|
|
toolCallId: 'call-1',
|
|
status: 'completed',
|
|
updates: [{ type: 'output', data: { success: true } }],
|
|
},
|
|
];
|
|
|
|
result = builderMessages.processAssistantMessages(currentMessages, batch2, 'batch-2');
|
|
expect(result.thinkingMessage).toBe('aiAssistant.thinkingSteps.processingResults');
|
|
currentMessages = result.messages;
|
|
|
|
// Third batch: workflow updated
|
|
const batch3: ChatRequest.MessageResponse[] = [
|
|
{
|
|
role: 'assistant',
|
|
type: 'workflow-updated',
|
|
codeSnippet: '{"nodes": [{"name": "HTTP Request"}], "connections": {}}',
|
|
},
|
|
];
|
|
|
|
result = builderMessages.processAssistantMessages(currentMessages, batch3, 'batch-3');
|
|
expect(result.thinkingMessage).toBe('aiAssistant.thinkingSteps.processingResults');
|
|
currentMessages = result.messages;
|
|
|
|
// Fourth batch: final text response
|
|
const batch4: ChatRequest.MessageResponse[] = [
|
|
{
|
|
type: 'message',
|
|
role: 'assistant',
|
|
text: 'Done!',
|
|
},
|
|
];
|
|
|
|
result = builderMessages.processAssistantMessages(currentMessages, batch4, 'batch-4');
|
|
expect(result.shouldClearThinking).toBe(true);
|
|
expect(result.thinkingMessage).toBeUndefined();
|
|
|
|
// Final state should have all messages
|
|
expect(result.messages).toHaveLength(3);
|
|
expect(result.messages.find((m) => m.type === 'tool')).toBeTruthy();
|
|
expect(result.messages.find((m) => m.type === 'workflow-updated')).toBeTruthy();
|
|
expect(result.messages.find((m) => m.type === 'text')).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
describe('clearRatingLogic', () => {
|
|
it('should remove showRating and ratingStyle properties from text messages', () => {
|
|
const messages: ChatUI.AssistantMessage[] = [
|
|
{
|
|
id: 'msg-1',
|
|
role: 'assistant',
|
|
type: 'text',
|
|
content: 'Hello there!',
|
|
showRating: true,
|
|
ratingStyle: 'regular',
|
|
read: false,
|
|
},
|
|
{
|
|
id: 'msg-2',
|
|
role: 'assistant',
|
|
type: 'text',
|
|
content: 'How can I help?',
|
|
showRating: false,
|
|
ratingStyle: 'minimal',
|
|
read: false,
|
|
},
|
|
];
|
|
|
|
const result = builderMessages.clearRatingLogic(messages);
|
|
|
|
expect(result).toHaveLength(2);
|
|
expect(result[0]).toMatchObject({
|
|
id: 'msg-1',
|
|
role: 'assistant',
|
|
type: 'text',
|
|
content: 'Hello there!',
|
|
read: false,
|
|
});
|
|
expect(result[0]).not.toHaveProperty('showRating');
|
|
expect(result[0]).not.toHaveProperty('ratingStyle');
|
|
|
|
expect(result[1]).toMatchObject({
|
|
id: 'msg-2',
|
|
role: 'assistant',
|
|
type: 'text',
|
|
content: 'How can I help?',
|
|
read: false,
|
|
});
|
|
expect(result[1]).not.toHaveProperty('showRating');
|
|
expect(result[1]).not.toHaveProperty('ratingStyle');
|
|
});
|
|
|
|
it('should leave non-text messages unchanged', () => {
|
|
const messages: ChatUI.AssistantMessage[] = [
|
|
{
|
|
id: 'tool-1',
|
|
role: 'assistant',
|
|
type: 'tool',
|
|
toolName: 'add_nodes',
|
|
toolCallId: 'call-1',
|
|
status: 'completed',
|
|
updates: [],
|
|
read: false,
|
|
},
|
|
{
|
|
id: 'workflow-1',
|
|
role: 'assistant',
|
|
type: 'workflow-updated',
|
|
codeSnippet: '{}',
|
|
read: false,
|
|
},
|
|
];
|
|
|
|
const result = builderMessages.clearRatingLogic(messages);
|
|
|
|
expect(result).toHaveLength(2);
|
|
expect(result[0]).toEqual(messages[0]);
|
|
expect(result[1]).toEqual(messages[1]);
|
|
});
|
|
|
|
it('should handle text messages without rating properties', () => {
|
|
const messages: ChatUI.AssistantMessage[] = [
|
|
{
|
|
id: 'msg-1',
|
|
role: 'assistant',
|
|
type: 'text',
|
|
content: 'No rating here',
|
|
read: false,
|
|
},
|
|
];
|
|
|
|
const result = builderMessages.clearRatingLogic(messages);
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0]).toEqual(messages[0]);
|
|
});
|
|
|
|
it('should handle empty message array', () => {
|
|
const result = builderMessages.clearRatingLogic([]);
|
|
expect(result).toHaveLength(0);
|
|
});
|
|
|
|
it('should handle mixed message types with some having rating properties', () => {
|
|
const messages: ChatUI.AssistantMessage[] = [
|
|
{
|
|
id: 'msg-1',
|
|
role: 'assistant',
|
|
type: 'text',
|
|
content: 'With rating',
|
|
showRating: true,
|
|
ratingStyle: 'regular',
|
|
read: false,
|
|
},
|
|
{
|
|
id: 'tool-1',
|
|
role: 'assistant',
|
|
type: 'tool',
|
|
toolName: 'test_tool',
|
|
toolCallId: 'call-1',
|
|
status: 'completed',
|
|
updates: [],
|
|
read: false,
|
|
},
|
|
{
|
|
id: 'msg-2',
|
|
role: 'assistant',
|
|
type: 'text',
|
|
content: 'Without rating',
|
|
read: false,
|
|
},
|
|
];
|
|
|
|
const result = builderMessages.clearRatingLogic(messages);
|
|
|
|
expect(result).toHaveLength(3);
|
|
expect(result[0]).not.toHaveProperty('showRating');
|
|
expect(result[0]).not.toHaveProperty('ratingStyle');
|
|
expect(result[1]).toEqual(messages[1]); // tool message unchanged
|
|
expect(result[2]).toEqual(messages[2]); // text without rating unchanged
|
|
});
|
|
});
|
|
|
|
describe('error message handling with retry', () => {
|
|
it('should pass retry function to error messages from processAssistantMessages', () => {
|
|
const retryFn = vi.fn(async () => {});
|
|
const currentMessages: ChatUI.AssistantMessage[] = [];
|
|
const newMessages: ChatRequest.MessageResponse[] = [
|
|
{
|
|
type: 'error',
|
|
role: 'assistant',
|
|
content: 'Something went wrong',
|
|
},
|
|
];
|
|
|
|
const result = builderMessages.processAssistantMessages(
|
|
currentMessages,
|
|
newMessages,
|
|
'test-id',
|
|
retryFn,
|
|
);
|
|
|
|
expect(result.messages).toHaveLength(1);
|
|
const errorMessage = result.messages[0] as ChatUI.ErrorMessage;
|
|
expect(errorMessage).toMatchObject({
|
|
id: 'test-id-0',
|
|
role: 'assistant',
|
|
type: 'error',
|
|
content: 'Something went wrong',
|
|
read: false,
|
|
});
|
|
expect(errorMessage.retry).toBe(retryFn);
|
|
});
|
|
|
|
it('should not pass retry function to non-error messages', () => {
|
|
const retryFn = vi.fn(async () => {});
|
|
const currentMessages: ChatUI.AssistantMessage[] = [];
|
|
const newMessages: ChatRequest.MessageResponse[] = [
|
|
{
|
|
type: 'message',
|
|
role: 'assistant',
|
|
text: 'This is a normal text message',
|
|
},
|
|
{
|
|
role: 'assistant',
|
|
type: 'tool',
|
|
toolName: 'add_nodes',
|
|
toolCallId: 'call-1',
|
|
status: 'running',
|
|
updates: [],
|
|
},
|
|
];
|
|
|
|
const result = builderMessages.processAssistantMessages(
|
|
currentMessages,
|
|
newMessages,
|
|
'test-id',
|
|
retryFn,
|
|
);
|
|
|
|
expect(result.messages).toHaveLength(2);
|
|
|
|
const textMessage = result.messages[0] as ChatUI.TextMessage;
|
|
expect(textMessage.type).toBe('text');
|
|
expect('retry' in textMessage).toBe(false);
|
|
|
|
const toolMessage = result.messages[1] as ChatUI.ToolMessage;
|
|
expect(toolMessage.type).toBe('tool');
|
|
expect('retry' in toolMessage).toBe(false);
|
|
});
|
|
|
|
it('should clear retry from previous error messages when processing new messages', () => {
|
|
const oldRetryFn = vi.fn(async () => {});
|
|
const newRetryFn = vi.fn(async () => {});
|
|
|
|
const currentMessages: ChatUI.AssistantMessage[] = [
|
|
{
|
|
id: 'error-1',
|
|
role: 'assistant',
|
|
type: 'error',
|
|
content: 'First error',
|
|
retry: oldRetryFn,
|
|
read: false,
|
|
} as ChatUI.ErrorMessage,
|
|
{
|
|
id: 'error-2',
|
|
role: 'assistant',
|
|
type: 'error',
|
|
content: 'Second error',
|
|
retry: oldRetryFn,
|
|
read: false,
|
|
} as ChatUI.ErrorMessage,
|
|
];
|
|
|
|
const newMessages: ChatRequest.MessageResponse[] = [
|
|
{
|
|
type: 'error',
|
|
role: 'assistant',
|
|
content: 'New error',
|
|
},
|
|
];
|
|
|
|
const result = builderMessages.processAssistantMessages(
|
|
currentMessages,
|
|
newMessages,
|
|
'test-id',
|
|
newRetryFn,
|
|
);
|
|
|
|
expect(result.messages).toHaveLength(3);
|
|
|
|
// First error should have retry removed
|
|
const firstError = result.messages[0] as ChatUI.ErrorMessage;
|
|
expect(firstError.content).toBe('First error');
|
|
expect('retry' in firstError).toBe(false);
|
|
|
|
// Second error should have retry removed
|
|
const secondError = result.messages[1] as ChatUI.ErrorMessage;
|
|
expect(secondError.content).toBe('Second error');
|
|
expect('retry' in secondError).toBe(false);
|
|
|
|
// New error should have the new retry function
|
|
const newError = result.messages[2] as ChatUI.ErrorMessage;
|
|
expect(newError.content).toBe('New error');
|
|
expect(newError.retry).toBe(newRetryFn);
|
|
});
|
|
|
|
it('should only keep retry on the last error message when multiple errors exist', () => {
|
|
const retryFn = vi.fn(async () => {});
|
|
const currentMessages: ChatUI.AssistantMessage[] = [];
|
|
const newMessages: ChatRequest.MessageResponse[] = [
|
|
{
|
|
type: 'error',
|
|
role: 'assistant',
|
|
content: 'First error in batch',
|
|
},
|
|
{
|
|
type: 'error',
|
|
role: 'assistant',
|
|
content: 'Second error in batch',
|
|
},
|
|
{
|
|
type: 'error',
|
|
role: 'assistant',
|
|
content: 'Third error in batch',
|
|
},
|
|
];
|
|
|
|
const result = builderMessages.processAssistantMessages(
|
|
currentMessages,
|
|
newMessages,
|
|
'test-id',
|
|
retryFn,
|
|
);
|
|
|
|
expect(result.messages).toHaveLength(3);
|
|
|
|
// First error should not have retry
|
|
const firstError = result.messages[0] as ChatUI.ErrorMessage;
|
|
expect(firstError.content).toBe('First error in batch');
|
|
expect('retry' in firstError).toBe(false);
|
|
|
|
// Second error should not have retry
|
|
const secondError = result.messages[1] as ChatUI.ErrorMessage;
|
|
expect(secondError.content).toBe('Second error in batch');
|
|
expect('retry' in secondError).toBe(false);
|
|
|
|
// Only the last error should have retry
|
|
const lastError = result.messages[2] as ChatUI.ErrorMessage;
|
|
expect(lastError.content).toBe('Third error in batch');
|
|
expect(lastError.retry).toBe(retryFn);
|
|
});
|
|
|
|
it('should handle mixed message types and only affect error messages with retry logic', () => {
|
|
const retryFn = vi.fn(async () => {});
|
|
const currentMessages: ChatUI.AssistantMessage[] = [
|
|
{
|
|
id: 'msg-1',
|
|
role: 'assistant',
|
|
type: 'text',
|
|
content: 'Normal message',
|
|
read: false,
|
|
},
|
|
{
|
|
id: 'error-1',
|
|
role: 'assistant',
|
|
type: 'error',
|
|
content: 'Old error',
|
|
retry: retryFn,
|
|
read: false,
|
|
} as ChatUI.ErrorMessage,
|
|
];
|
|
|
|
const newMessages: ChatRequest.MessageResponse[] = [
|
|
{
|
|
type: 'message',
|
|
role: 'assistant',
|
|
text: 'New text message',
|
|
},
|
|
{
|
|
type: 'error',
|
|
role: 'assistant',
|
|
content: 'New error message',
|
|
},
|
|
];
|
|
|
|
const result = builderMessages.processAssistantMessages(
|
|
currentMessages,
|
|
newMessages,
|
|
'test-id',
|
|
retryFn,
|
|
);
|
|
|
|
expect(result.messages).toHaveLength(4);
|
|
|
|
// Normal text message should be unchanged
|
|
expect(result.messages[0]).toMatchObject({
|
|
type: 'text',
|
|
content: 'Normal message',
|
|
});
|
|
expect('retry' in result.messages[0]).toBe(false);
|
|
|
|
// Old error should have retry removed
|
|
const oldError = result.messages[1] as ChatUI.ErrorMessage;
|
|
expect(oldError.content).toBe('Old error');
|
|
expect('retry' in oldError).toBe(false);
|
|
|
|
// New text message should not have retry
|
|
expect(result.messages[2]).toMatchObject({
|
|
type: 'text',
|
|
content: 'New text message',
|
|
});
|
|
expect('retry' in result.messages[2]).toBe(false);
|
|
|
|
// Only the new error should have retry
|
|
const newError = result.messages[3] as ChatUI.ErrorMessage;
|
|
expect(newError.content).toBe('New error message');
|
|
expect(newError.retry).toBe(retryFn);
|
|
});
|
|
});
|
|
|
|
describe('applyRatingLogic', () => {
|
|
it('should apply rating to the last assistant text message after workflow-updated when no tools are running', () => {
|
|
const messages: ChatUI.AssistantMessage[] = [
|
|
{
|
|
id: 'msg-1',
|
|
role: 'assistant',
|
|
type: 'text',
|
|
content: 'Starting process...',
|
|
read: false,
|
|
},
|
|
{
|
|
id: 'workflow-1',
|
|
role: 'assistant',
|
|
type: 'workflow-updated',
|
|
codeSnippet: '{"nodes": [], "connections": {}}',
|
|
read: false,
|
|
},
|
|
{
|
|
id: 'msg-2',
|
|
role: 'assistant',
|
|
type: 'text',
|
|
content: 'Process completed!',
|
|
read: false,
|
|
},
|
|
];
|
|
|
|
const result = builderMessages.applyRatingLogic(messages);
|
|
|
|
expect(result).toHaveLength(3);
|
|
expect(result[0].showRating).toBeUndefined();
|
|
expect(result[1].showRating).toBeUndefined();
|
|
expect(result[1].type).toBe('workflow-updated');
|
|
expect(result[2]).toMatchObject({
|
|
id: 'msg-2',
|
|
content: 'Process completed!',
|
|
showRating: true,
|
|
ratingStyle: 'regular',
|
|
});
|
|
});
|
|
|
|
it('should not apply rating when tools are still running', () => {
|
|
const messages: ChatUI.AssistantMessage[] = [
|
|
{
|
|
id: 'workflow-1',
|
|
role: 'assistant',
|
|
type: 'workflow-updated',
|
|
codeSnippet: '{}',
|
|
read: false,
|
|
},
|
|
{
|
|
id: 'tool-1',
|
|
role: 'assistant',
|
|
type: 'tool',
|
|
toolName: 'add_nodes',
|
|
toolCallId: 'call-1',
|
|
status: 'running',
|
|
updates: [],
|
|
read: false,
|
|
},
|
|
{
|
|
id: 'msg-1',
|
|
role: 'assistant',
|
|
type: 'text',
|
|
content: 'Working on it...',
|
|
read: false,
|
|
},
|
|
];
|
|
|
|
const result = builderMessages.applyRatingLogic(messages);
|
|
|
|
expect(result).toHaveLength(3);
|
|
result.forEach((message) => {
|
|
expect(message.showRating).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
it('should not apply rating when still thinking (tools completed but no text response)', () => {
|
|
const messages: ChatUI.AssistantMessage[] = [
|
|
{
|
|
id: 'workflow-1',
|
|
role: 'assistant',
|
|
type: 'workflow-updated',
|
|
codeSnippet: '{}',
|
|
read: false,
|
|
},
|
|
{
|
|
id: 'tool-1',
|
|
role: 'assistant',
|
|
type: 'tool',
|
|
toolName: 'add_nodes',
|
|
toolCallId: 'call-1',
|
|
status: 'completed',
|
|
updates: [],
|
|
read: false,
|
|
},
|
|
];
|
|
|
|
const result = builderMessages.applyRatingLogic(messages);
|
|
|
|
expect(result).toHaveLength(2);
|
|
result.forEach((message) => {
|
|
expect(message.showRating).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
it('should not apply rating when no workflow-updated message exists', () => {
|
|
const messages: ChatUI.AssistantMessage[] = [
|
|
{
|
|
id: 'msg-1',
|
|
role: 'assistant',
|
|
type: 'text',
|
|
content: 'Hello there!',
|
|
read: false,
|
|
},
|
|
{
|
|
id: 'msg-2',
|
|
role: 'assistant',
|
|
type: 'text',
|
|
content: 'How can I help?',
|
|
read: false,
|
|
},
|
|
];
|
|
|
|
const result = builderMessages.applyRatingLogic(messages);
|
|
|
|
expect(result).toHaveLength(2);
|
|
expect(result[0].showRating).toBeUndefined();
|
|
expect(result[1].showRating).toBeUndefined();
|
|
});
|
|
|
|
it('should remove existing ratings when tools are running', () => {
|
|
const messages: ChatUI.AssistantMessage[] = [
|
|
{
|
|
id: 'msg-1',
|
|
role: 'assistant',
|
|
type: 'text',
|
|
content: 'Previous message',
|
|
showRating: true,
|
|
ratingStyle: 'regular',
|
|
read: false,
|
|
},
|
|
{
|
|
id: 'tool-1',
|
|
role: 'assistant',
|
|
type: 'tool',
|
|
toolName: 'add_nodes',
|
|
toolCallId: 'call-1',
|
|
status: 'running',
|
|
updates: [],
|
|
read: false,
|
|
},
|
|
];
|
|
|
|
const result = builderMessages.applyRatingLogic(messages);
|
|
|
|
expect(result).toHaveLength(2);
|
|
expect(result[0]).not.toHaveProperty('showRating');
|
|
expect(result[0]).not.toHaveProperty('ratingStyle');
|
|
});
|
|
|
|
it('should remove ratings from non-target messages when applying rating to target message', () => {
|
|
const messages: ChatUI.AssistantMessage[] = [
|
|
{
|
|
id: 'msg-1',
|
|
role: 'assistant',
|
|
type: 'text',
|
|
content: 'Earlier message',
|
|
showRating: true,
|
|
ratingStyle: 'minimal',
|
|
read: false,
|
|
},
|
|
{
|
|
id: 'workflow-1',
|
|
role: 'assistant',
|
|
type: 'workflow-updated',
|
|
codeSnippet: '{}',
|
|
read: false,
|
|
},
|
|
{
|
|
id: 'msg-2',
|
|
role: 'assistant',
|
|
type: 'text',
|
|
content: 'Target message',
|
|
read: false,
|
|
},
|
|
];
|
|
|
|
const result = builderMessages.applyRatingLogic(messages);
|
|
|
|
expect(result).toHaveLength(3);
|
|
expect(result[0]).not.toHaveProperty('showRating');
|
|
expect(result[0]).not.toHaveProperty('ratingStyle');
|
|
expect(result[2]).toMatchObject({
|
|
showRating: true,
|
|
ratingStyle: 'regular',
|
|
});
|
|
});
|
|
|
|
it('should handle multiple workflow-updated messages and apply rating after the last one', () => {
|
|
const messages: ChatUI.AssistantMessage[] = [
|
|
{
|
|
id: 'workflow-1',
|
|
role: 'assistant',
|
|
type: 'workflow-updated',
|
|
codeSnippet: '{"nodes": []}',
|
|
read: false,
|
|
},
|
|
{
|
|
id: 'msg-1',
|
|
role: 'assistant',
|
|
type: 'text',
|
|
content: 'First update done',
|
|
read: false,
|
|
},
|
|
{
|
|
id: 'workflow-2',
|
|
role: 'assistant',
|
|
type: 'workflow-updated',
|
|
codeSnippet: '{"nodes": [{"name": "HTTP"}]}',
|
|
read: false,
|
|
},
|
|
{
|
|
id: 'msg-2',
|
|
role: 'assistant',
|
|
type: 'text',
|
|
content: 'Final update complete',
|
|
read: false,
|
|
},
|
|
];
|
|
|
|
const result = builderMessages.applyRatingLogic(messages);
|
|
|
|
expect(result).toHaveLength(4);
|
|
expect(result[1].showRating).toBeUndefined(); // First text message
|
|
expect(result[3]).toMatchObject({
|
|
content: 'Final update complete',
|
|
showRating: true,
|
|
ratingStyle: 'regular',
|
|
});
|
|
});
|
|
|
|
it('should handle user messages mixed with assistant messages', () => {
|
|
const messages: ChatUI.AssistantMessage[] = [
|
|
{
|
|
id: 'user-1',
|
|
role: 'user',
|
|
type: 'text',
|
|
content: 'Create a workflow',
|
|
read: true,
|
|
},
|
|
{
|
|
id: 'workflow-1',
|
|
role: 'assistant',
|
|
type: 'workflow-updated',
|
|
codeSnippet: '{}',
|
|
read: false,
|
|
},
|
|
{
|
|
id: 'msg-1',
|
|
role: 'assistant',
|
|
type: 'text',
|
|
content: 'Workflow created!',
|
|
read: false,
|
|
},
|
|
];
|
|
|
|
const result = builderMessages.applyRatingLogic(messages);
|
|
|
|
expect(result).toHaveLength(3);
|
|
expect(result[0].showRating).toBeUndefined(); // User message
|
|
expect(result[2]).toMatchObject({
|
|
content: 'Workflow created!',
|
|
showRating: true,
|
|
ratingStyle: 'regular',
|
|
});
|
|
});
|
|
|
|
it('should handle empty message array', () => {
|
|
const result = builderMessages.applyRatingLogic([]);
|
|
expect(result).toHaveLength(0);
|
|
});
|
|
|
|
it('should apply rating only to assistant text messages, not user text messages', () => {
|
|
const messages: ChatUI.AssistantMessage[] = [
|
|
{
|
|
id: 'workflow-1',
|
|
role: 'assistant',
|
|
type: 'workflow-updated',
|
|
codeSnippet: '{}',
|
|
read: false,
|
|
},
|
|
{
|
|
id: 'user-1',
|
|
role: 'user',
|
|
type: 'text',
|
|
content: 'Thanks!',
|
|
read: true,
|
|
},
|
|
{
|
|
id: 'msg-1',
|
|
role: 'assistant',
|
|
type: 'text',
|
|
content: 'You are welcome!',
|
|
read: false,
|
|
},
|
|
];
|
|
|
|
const result = builderMessages.applyRatingLogic(messages);
|
|
|
|
expect(result).toHaveLength(3);
|
|
expect(result[1].showRating).toBeUndefined(); // User message should not have rating
|
|
expect(result[2]).toMatchObject({
|
|
content: 'You are welcome!',
|
|
showRating: true,
|
|
ratingStyle: 'regular',
|
|
});
|
|
});
|
|
});
|
|
});
|