mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 09:36:44 +00:00
782 lines
24 KiB
TypeScript
782 lines
24 KiB
TypeScript
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
|
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
import { setActivePinia, createPinia } from 'pinia';
|
|
import { ENABLED_VIEWS, useBuilderStore } from '@/stores/builder.store';
|
|
import { usePostHog } from './posthog.store';
|
|
import { useSettingsStore } from '@/stores/settings.store';
|
|
import { defaultSettings } from '../__tests__/defaults';
|
|
import merge from 'lodash/merge';
|
|
import { DEFAULT_POSTHOG_SETTINGS } from './posthog.test';
|
|
import { WORKFLOW_BUILDER_EXPERIMENT } from '@/constants';
|
|
import { reactive } from 'vue';
|
|
import * as chatAPI from '@/api/ai';
|
|
import * as telemetryModule from '@/composables/useTelemetry';
|
|
import type { Telemetry } from '@/plugins/telemetry';
|
|
import type { ChatUI } from '@n8n/design-system/types/assistant';
|
|
import { DEFAULT_CHAT_WIDTH, MAX_CHAT_WIDTH, MIN_CHAT_WIDTH } from './assistant.store';
|
|
|
|
// Mock useI18n to return the keys instead of translations
|
|
vi.mock('@n8n/i18n', () => ({
|
|
useI18n: () => ({
|
|
baseText: (key: string) => key,
|
|
}),
|
|
}));
|
|
|
|
let settingsStore: ReturnType<typeof useSettingsStore>;
|
|
let posthogStore: ReturnType<typeof usePostHog>;
|
|
|
|
const apiSpy = vi.spyOn(chatAPI, 'chatWithBuilder');
|
|
|
|
const track = vi.fn();
|
|
const spy = vi.spyOn(telemetryModule, 'useTelemetry');
|
|
spy.mockImplementation(
|
|
() =>
|
|
({
|
|
track,
|
|
}) as unknown as Telemetry,
|
|
);
|
|
|
|
const setAssistantEnabled = (enabled: boolean) => {
|
|
settingsStore.setSettings(
|
|
merge({}, defaultSettings, {
|
|
aiAssistant: { enabled },
|
|
}),
|
|
);
|
|
};
|
|
|
|
const currentRouteName = ENABLED_VIEWS[0];
|
|
vi.mock('vue-router', () => ({
|
|
useRoute: vi.fn(() =>
|
|
reactive({
|
|
path: '/',
|
|
params: {},
|
|
name: currentRouteName,
|
|
}),
|
|
),
|
|
useRouter: vi.fn(),
|
|
RouterLink: vi.fn(),
|
|
}));
|
|
|
|
describe('AI Builder store', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
setActivePinia(createPinia());
|
|
settingsStore = useSettingsStore();
|
|
settingsStore.setSettings(
|
|
merge({}, defaultSettings, {
|
|
posthog: DEFAULT_POSTHOG_SETTINGS,
|
|
}),
|
|
);
|
|
window.posthog = {
|
|
init: () => {},
|
|
identify: () => {},
|
|
};
|
|
posthogStore = usePostHog();
|
|
posthogStore.init();
|
|
track.mockReset();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.clearAllMocks();
|
|
vi.clearAllTimers();
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it('initializes with default values', () => {
|
|
const builderStore = useBuilderStore();
|
|
|
|
expect(builderStore.chatWidth).toBe(DEFAULT_CHAT_WIDTH);
|
|
expect(builderStore.chatMessages).toEqual([]);
|
|
expect(builderStore.chatWindowOpen).toBe(false);
|
|
expect(builderStore.streaming).toBe(false);
|
|
});
|
|
|
|
it('can change chat width', () => {
|
|
const builderStore = useBuilderStore();
|
|
|
|
builderStore.updateWindowWidth(400);
|
|
expect(builderStore.chatWidth).toBe(400);
|
|
});
|
|
|
|
it('should not allow chat width to be less than the minimal width', () => {
|
|
const builderStore = useBuilderStore();
|
|
|
|
builderStore.updateWindowWidth(100);
|
|
expect(builderStore.chatWidth).toBe(MIN_CHAT_WIDTH);
|
|
});
|
|
|
|
it('should not allow chat width to be more than the maximal width', () => {
|
|
const builderStore = useBuilderStore();
|
|
|
|
builderStore.updateWindowWidth(2000);
|
|
expect(builderStore.chatWidth).toBe(MAX_CHAT_WIDTH);
|
|
});
|
|
|
|
it('should open chat window', async () => {
|
|
const builderStore = useBuilderStore();
|
|
|
|
await builderStore.openChat();
|
|
expect(builderStore.chatWindowOpen).toBe(true);
|
|
});
|
|
|
|
it('should close chat window', () => {
|
|
const builderStore = useBuilderStore();
|
|
|
|
builderStore.closeChat();
|
|
expect(builderStore.chatWindowOpen).toBe(false);
|
|
});
|
|
|
|
it('can process a simple assistant message through API', async () => {
|
|
const builderStore = useBuilderStore();
|
|
|
|
apiSpy.mockImplementationOnce((_ctx, _payload, onMessage, onDone) => {
|
|
onMessage({
|
|
messages: [
|
|
{
|
|
type: 'message',
|
|
role: 'assistant',
|
|
text: 'Hello!',
|
|
},
|
|
],
|
|
sessionId: 'test-session',
|
|
});
|
|
onDone();
|
|
});
|
|
|
|
builderStore.sendChatMessage({ text: 'Hi' });
|
|
await vi.waitFor(() => expect(builderStore.chatMessages.length).toBe(2));
|
|
expect(builderStore.chatMessages[0].role).toBe('user');
|
|
expect(builderStore.chatMessages[1]).toMatchObject({
|
|
type: 'text',
|
|
role: 'assistant',
|
|
content: 'Hello!',
|
|
read: false,
|
|
});
|
|
});
|
|
|
|
it('can process a workflow-updated message through API', async () => {
|
|
const builderStore = useBuilderStore();
|
|
|
|
apiSpy.mockImplementationOnce((_ctx, _payload, onMessage, onDone) => {
|
|
onMessage({
|
|
messages: [
|
|
{
|
|
type: 'workflow-updated',
|
|
role: 'assistant',
|
|
codeSnippet: '{"nodes":[],"connections":[]}',
|
|
},
|
|
],
|
|
sessionId: 'test-session',
|
|
});
|
|
onDone();
|
|
});
|
|
|
|
builderStore.sendChatMessage({ text: 'Create workflow' });
|
|
await vi.waitFor(() => expect(builderStore.chatMessages.length).toBe(2));
|
|
expect(builderStore.chatMessages[1]).toMatchObject({
|
|
type: 'workflow-updated',
|
|
role: 'assistant',
|
|
codeSnippet: '{"nodes":[],"connections":[]}',
|
|
read: false,
|
|
});
|
|
|
|
// Verify workflow messages are accessible via computed property
|
|
expect(builderStore.workflowMessages.length).toBe(1);
|
|
});
|
|
|
|
it('should show processing results message when tools complete', async () => {
|
|
vi.useFakeTimers();
|
|
const builderStore = useBuilderStore();
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
let onMessageCallback: any;
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
let onDoneCallback: any;
|
|
|
|
apiSpy.mockImplementationOnce((_ctx, _payload, onMessage, onDone) => {
|
|
onMessageCallback = onMessage;
|
|
onDoneCallback = onDone;
|
|
});
|
|
|
|
builderStore.sendChatMessage({ text: 'Add nodes and connect them' });
|
|
|
|
// Initially shows "aiAssistant.thinkingSteps.thinking" from prepareForStreaming
|
|
expect(builderStore.assistantThinkingMessage).toBe('aiAssistant.thinkingSteps.thinking');
|
|
|
|
// First tool starts
|
|
onMessageCallback({
|
|
messages: [
|
|
{
|
|
type: 'tool',
|
|
role: 'assistant',
|
|
toolName: 'add_nodes',
|
|
toolCallId: 'call-1',
|
|
status: 'running',
|
|
updates: [{ type: 'input', data: {} }],
|
|
},
|
|
],
|
|
});
|
|
|
|
// Should show "aiAssistant.thinkingSteps.runningTools"
|
|
expect(builderStore.assistantThinkingMessage).toBe('aiAssistant.thinkingSteps.runningTools');
|
|
|
|
// Second tool starts (different toolCallId)
|
|
onMessageCallback({
|
|
messages: [
|
|
{
|
|
type: 'tool',
|
|
role: 'assistant',
|
|
toolName: 'connect_nodes',
|
|
toolCallId: 'call-2',
|
|
status: 'running',
|
|
updates: [{ type: 'input', data: {} }],
|
|
},
|
|
],
|
|
});
|
|
|
|
// Still showing "aiAssistant.thinkingSteps.runningTools" with multiple tools
|
|
expect(builderStore.assistantThinkingMessage).toBe('aiAssistant.thinkingSteps.runningTools');
|
|
|
|
// First tool completes
|
|
onMessageCallback({
|
|
messages: [
|
|
{
|
|
type: 'tool',
|
|
role: 'assistant',
|
|
toolName: 'add_nodes',
|
|
toolCallId: 'call-1',
|
|
status: 'completed',
|
|
updates: [{ type: 'output', data: { success: true } }],
|
|
},
|
|
],
|
|
});
|
|
|
|
// Still "aiAssistant.thinkingSteps.runningTools" because second tool is still running
|
|
expect(builderStore.assistantThinkingMessage).toBe('aiAssistant.thinkingSteps.runningTools');
|
|
|
|
// Second tool completes
|
|
onMessageCallback({
|
|
messages: [
|
|
{
|
|
type: 'tool',
|
|
role: 'assistant',
|
|
toolName: 'connect_nodes',
|
|
toolCallId: 'call-2',
|
|
status: 'completed',
|
|
updates: [{ type: 'output', data: { success: true } }],
|
|
},
|
|
],
|
|
});
|
|
|
|
// Now should show "aiAssistant.thinkingSteps.processingResults" because all tools completed
|
|
expect(builderStore.assistantThinkingMessage).toBe(
|
|
'aiAssistant.thinkingSteps.processingResults',
|
|
);
|
|
|
|
// Call onDone to stop streaming
|
|
onDoneCallback();
|
|
|
|
// Message should persist after streaming ends
|
|
expect(builderStore.streaming).toBe(false);
|
|
expect(builderStore.assistantThinkingMessage).toBe(
|
|
'aiAssistant.thinkingSteps.processingResults',
|
|
);
|
|
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it('should keep processing message when workflow-updated arrives', async () => {
|
|
const builderStore = useBuilderStore();
|
|
|
|
apiSpy.mockImplementationOnce((_ctx, _payload, onMessage, onDone) => {
|
|
// Tool completes
|
|
onMessage({
|
|
messages: [
|
|
{
|
|
type: 'tool',
|
|
role: 'assistant',
|
|
toolName: 'add_nodes',
|
|
toolCallId: 'call-1',
|
|
status: 'completed',
|
|
updates: [{ type: 'output', data: { success: true } }],
|
|
},
|
|
],
|
|
});
|
|
|
|
// Workflow update arrives
|
|
onMessage({
|
|
messages: [
|
|
{
|
|
type: 'workflow-updated',
|
|
role: 'assistant',
|
|
codeSnippet: '{"nodes": [], "connections": {}}',
|
|
},
|
|
],
|
|
});
|
|
|
|
// Call onDone to stop streaming
|
|
onDone();
|
|
});
|
|
|
|
builderStore.sendChatMessage({ text: 'Add a node' });
|
|
|
|
// Should show "aiAssistant.thinkingSteps.processingResults" when tool completes
|
|
await vi.waitFor(() =>
|
|
expect(builderStore.assistantThinkingMessage).toBe(
|
|
'aiAssistant.thinkingSteps.processingResults',
|
|
),
|
|
);
|
|
|
|
// Should still show "aiAssistant.thinkingSteps.processingResults" after workflow-updated
|
|
await vi.waitFor(() => expect(builderStore.chatMessages).toHaveLength(3)); // user + tool + workflow
|
|
expect(builderStore.assistantThinkingMessage).toBe(
|
|
'aiAssistant.thinkingSteps.processingResults',
|
|
);
|
|
|
|
// Verify streaming has ended
|
|
expect(builderStore.streaming).toBe(false);
|
|
});
|
|
|
|
it('should reset builder chat session', async () => {
|
|
const builderStore = useBuilderStore();
|
|
|
|
apiSpy.mockImplementationOnce((_ctx, _payload, onMessage, onDone) => {
|
|
onMessage({
|
|
messages: [
|
|
{
|
|
type: 'message',
|
|
role: 'assistant',
|
|
text: 'Hello!',
|
|
quickReplies: [
|
|
{ text: 'Yes', type: 'text' },
|
|
{ text: 'No', type: 'text' },
|
|
],
|
|
},
|
|
],
|
|
sessionId: 'test-session',
|
|
});
|
|
onDone();
|
|
});
|
|
|
|
builderStore.sendChatMessage({ text: 'Hi' });
|
|
await vi.waitFor(() => expect(builderStore.chatMessages.length).toBe(2));
|
|
|
|
builderStore.resetBuilderChat();
|
|
expect(builderStore.chatMessages).toEqual([]);
|
|
expect(builderStore.assistantThinkingMessage).toBeUndefined();
|
|
});
|
|
|
|
it('should not show builder if disabled in settings', () => {
|
|
const builderStore = useBuilderStore();
|
|
|
|
setAssistantEnabled(false);
|
|
expect(builderStore.isAssistantEnabled).toBe(false);
|
|
expect(builderStore.canShowAssistant).toBe(false);
|
|
expect(builderStore.canShowAssistantButtonsOnCanvas).toBe(false);
|
|
});
|
|
|
|
it('should show builder if all conditions are met', () => {
|
|
const builderStore = useBuilderStore();
|
|
|
|
setAssistantEnabled(true);
|
|
expect(builderStore.isAssistantEnabled).toBe(true);
|
|
expect(builderStore.canShowAssistant).toBe(true);
|
|
expect(builderStore.canShowAssistantButtonsOnCanvas).toBe(true);
|
|
});
|
|
|
|
// Split into two separate tests to avoid caching issues with computed properties
|
|
it('should return true when experiment flag is set to variant', () => {
|
|
const builderStore = useBuilderStore();
|
|
vi.spyOn(posthogStore, 'getVariant').mockReturnValue(WORKFLOW_BUILDER_EXPERIMENT.variant);
|
|
expect(builderStore.isAIBuilderEnabled).toBe(true);
|
|
});
|
|
|
|
it('should return false when experiment flag is set to control', () => {
|
|
const builderStore = useBuilderStore();
|
|
vi.spyOn(posthogStore, 'getVariant').mockReturnValue(WORKFLOW_BUILDER_EXPERIMENT.control);
|
|
expect(builderStore.isAIBuilderEnabled).toBe(false);
|
|
});
|
|
|
|
it('should initialize builder chat session with prompt', async () => {
|
|
const builderStore = useBuilderStore();
|
|
const mockSessionId = 'test-session-id';
|
|
|
|
apiSpy.mockImplementation((_ctx, _payload, onMessage, onDone) => {
|
|
onMessage({
|
|
messages: [
|
|
{
|
|
type: 'message',
|
|
role: 'assistant',
|
|
text: 'How can I help you build a workflow?',
|
|
},
|
|
],
|
|
sessionId: mockSessionId,
|
|
});
|
|
onDone();
|
|
});
|
|
|
|
builderStore.sendChatMessage({ text: 'I want to build a workflow' });
|
|
await vi.waitFor(() => expect(builderStore.chatMessages.length).toBe(2));
|
|
|
|
expect(apiSpy).toHaveBeenCalled();
|
|
expect(builderStore.chatMessages[0].role).toBe('user');
|
|
expect(builderStore.chatMessages[1].role).toBe('assistant');
|
|
expect(builderStore.streaming).toBe(false);
|
|
});
|
|
|
|
it('should send a follow-up message in an existing session', async () => {
|
|
const builderStore = useBuilderStore();
|
|
const mockSessionId = 'test-session-id';
|
|
|
|
// Setup initial session
|
|
apiSpy.mockImplementationOnce((_ctx, _payload, onMessage, onDone) => {
|
|
onMessage({
|
|
messages: [
|
|
{
|
|
type: 'message',
|
|
role: 'assistant',
|
|
text: 'How can I help you build a workflow?',
|
|
},
|
|
],
|
|
sessionId: mockSessionId,
|
|
});
|
|
onDone();
|
|
});
|
|
|
|
// Setup follow-up message response
|
|
apiSpy.mockImplementationOnce((_ctx, _payload, onMessage, onDone) => {
|
|
onMessage({
|
|
messages: [
|
|
{
|
|
type: 'message',
|
|
role: 'assistant',
|
|
text: 'Here are some workflow ideas',
|
|
},
|
|
],
|
|
sessionId: mockSessionId,
|
|
});
|
|
onDone();
|
|
});
|
|
|
|
builderStore.sendChatMessage({ text: 'I want to build a workflow' });
|
|
await vi.waitFor(() => expect(builderStore.chatMessages.length).toBe(2));
|
|
|
|
// Send a follow-up message
|
|
builderStore.sendChatMessage({ text: 'Generate a workflow for me' });
|
|
await vi.waitFor(() => expect(builderStore.chatMessages.length).toBe(4));
|
|
|
|
const thirdMessage = builderStore.chatMessages[2] as ChatUI.TextMessage;
|
|
const fourthMessage = builderStore.chatMessages[3] as ChatUI.TextMessage;
|
|
expect(thirdMessage.role).toBe('user');
|
|
expect(thirdMessage.type).toBe('text');
|
|
expect(thirdMessage.content).toBe('Generate a workflow for me');
|
|
expect(fourthMessage.role).toBe('assistant');
|
|
expect(fourthMessage.type).toBe('text');
|
|
expect(fourthMessage.content).toBe('Here are some workflow ideas');
|
|
});
|
|
|
|
it('should properly handle errors in chat session', async () => {
|
|
const builderStore = useBuilderStore();
|
|
|
|
// Simulate an error response
|
|
apiSpy.mockImplementationOnce((_ctx, _payload, _onMessage, _onDone, onError) => {
|
|
onError(new Error('An API error occurred'));
|
|
});
|
|
|
|
builderStore.sendChatMessage({ text: 'I want to build a workflow' });
|
|
await vi.waitFor(() => expect(builderStore.chatMessages.length).toBe(2));
|
|
expect(builderStore.chatMessages[0].role).toBe('user');
|
|
expect(builderStore.chatMessages[1].type).toBe('error');
|
|
|
|
// Error message should have a retry function
|
|
const errorMessage = builderStore.chatMessages[1] as ChatUI.ErrorMessage;
|
|
expect(errorMessage.retry).toBeDefined();
|
|
|
|
// Verify streaming state was reset
|
|
expect(builderStore.streaming).toBe(false);
|
|
|
|
// Set up a successful response for the retry
|
|
apiSpy.mockImplementationOnce((_ctx, _payload, onMessage, onDone) => {
|
|
onMessage({
|
|
messages: [
|
|
{
|
|
type: 'message',
|
|
role: 'assistant',
|
|
text: 'I can help you build a workflow',
|
|
},
|
|
],
|
|
sessionId: 'new-session',
|
|
});
|
|
onDone();
|
|
});
|
|
|
|
// Retry the failed request
|
|
if (errorMessage.retry) {
|
|
void errorMessage.retry();
|
|
await vi.waitFor(() => expect(builderStore.chatMessages.length).toBe(2));
|
|
}
|
|
expect(builderStore.chatMessages[0].role).toBe('user');
|
|
expect(builderStore.chatMessages[1].type).toBe('text');
|
|
expect((builderStore.chatMessages[1] as ChatUI.TextMessage).content).toBe(
|
|
'I can help you build a workflow',
|
|
);
|
|
});
|
|
|
|
describe('Abort functionality', () => {
|
|
it('should create and manage abort controller', () => {
|
|
const builderStore = useBuilderStore();
|
|
|
|
// Initially no abort controller (might be undefined or null)
|
|
expect(builderStore.streamingAbortController).toBeFalsy();
|
|
|
|
// Start streaming creates abort controller
|
|
apiSpy.mockImplementationOnce((_ctx, _payload, onMessage, _onDone, _onError, _signal) => {
|
|
// Simulate successful start of streaming
|
|
setTimeout(() => {
|
|
onMessage({
|
|
messages: [
|
|
{
|
|
type: 'message',
|
|
role: 'assistant',
|
|
text: 'Processing...',
|
|
},
|
|
],
|
|
sessionId: 'test-session',
|
|
});
|
|
}, 0);
|
|
});
|
|
|
|
builderStore.sendChatMessage({ text: 'test' });
|
|
expect(builderStore.streamingAbortController).not.toBeNull();
|
|
expect(builderStore.streamingAbortController).toBeInstanceOf(AbortController);
|
|
});
|
|
|
|
it('should call abort on existing controller when stopStreaming is called', () => {
|
|
const builderStore = useBuilderStore();
|
|
|
|
// First start a request to create an abort controller
|
|
apiSpy.mockImplementationOnce(() => {});
|
|
builderStore.sendChatMessage({ text: 'test' });
|
|
|
|
// Verify controller was created
|
|
const controller = builderStore.streamingAbortController;
|
|
expect(controller).toBeInstanceOf(AbortController);
|
|
|
|
// Spy on the abort method
|
|
const abortSpy = vi.spyOn(controller!, 'abort');
|
|
|
|
// Call stopStreaming
|
|
builderStore.stopStreaming();
|
|
|
|
// Verify abort was called
|
|
expect(abortSpy).toHaveBeenCalled();
|
|
expect(builderStore.streamingAbortController).toBeNull();
|
|
expect(builderStore.streaming).toBe(false);
|
|
});
|
|
|
|
it('should handle AbortError gracefully', async () => {
|
|
const builderStore = useBuilderStore();
|
|
|
|
// Simulate an abort error
|
|
const abortError = new Error('AbortError');
|
|
abortError.name = 'AbortError';
|
|
|
|
apiSpy.mockImplementationOnce((_ctx, _payload, _onMessage, _onDone, onError) => {
|
|
onError(abortError);
|
|
});
|
|
|
|
builderStore.sendChatMessage({ text: 'test message' });
|
|
await vi.waitFor(() => expect(builderStore.chatMessages.length).toBe(2));
|
|
|
|
// Should have user message and aborted message
|
|
expect(builderStore.chatMessages[0].role).toBe('user');
|
|
expect(builderStore.chatMessages[1].role).toBe('assistant');
|
|
expect(builderStore.chatMessages[1].type).toBe('text');
|
|
expect((builderStore.chatMessages[1] as ChatUI.TextMessage).content).toBe('[Task aborted]');
|
|
|
|
// Verify streaming state was reset
|
|
expect(builderStore.streaming).toBe(false);
|
|
expect(builderStore.assistantThinkingMessage).toBeUndefined();
|
|
});
|
|
|
|
it('should abort previous request when sending new message', () => {
|
|
const builderStore = useBuilderStore();
|
|
|
|
// The current implementation prevents sending a new message while streaming
|
|
// by checking if streaming.value is true and returning early.
|
|
// Mock for first request - keep it pending
|
|
apiSpy.mockImplementationOnce((_ctx, _payload, onMessage, _onDone) => {
|
|
// Don't call onDone to keep streaming active
|
|
setTimeout(() => {
|
|
onMessage({
|
|
messages: [
|
|
{
|
|
type: 'message',
|
|
role: 'assistant',
|
|
text: 'Processing first message...',
|
|
},
|
|
],
|
|
sessionId: 'test-session',
|
|
});
|
|
}, 10);
|
|
});
|
|
|
|
// Start first request
|
|
builderStore.sendChatMessage({ text: 'first message' });
|
|
|
|
// Verify streaming is active and controller was created
|
|
expect(builderStore.streaming).toBe(true);
|
|
const firstController = builderStore.streamingAbortController;
|
|
expect(firstController).not.toBeNull();
|
|
expect(firstController).toBeInstanceOf(AbortController);
|
|
|
|
// Track if abort was called
|
|
const abortSpy = vi.spyOn(firstController!, 'abort');
|
|
|
|
// Try to send second message while streaming - it should be ignored
|
|
builderStore.sendChatMessage({ text: 'second message ignored' });
|
|
|
|
// Verify the abort was NOT called and controller is the same
|
|
expect(abortSpy).not.toHaveBeenCalled();
|
|
expect(builderStore.streamingAbortController).toBe(firstController);
|
|
|
|
// Now properly stop streaming first
|
|
builderStore.stopStreaming();
|
|
|
|
// Verify abort was called and controller was cleared
|
|
expect(abortSpy).toHaveBeenCalled();
|
|
expect(builderStore.streamingAbortController).toBeNull();
|
|
expect(builderStore.streaming).toBe(false);
|
|
|
|
// Mock for second request
|
|
apiSpy.mockImplementationOnce(() => {});
|
|
|
|
// Now we can send a new message
|
|
builderStore.sendChatMessage({ text: 'second message' });
|
|
|
|
// New controller should be created
|
|
const secondController = builderStore.streamingAbortController;
|
|
expect(secondController).not.toBe(firstController);
|
|
expect(secondController).not.toBeNull();
|
|
expect(secondController).toBeInstanceOf(AbortController);
|
|
});
|
|
|
|
it('should pass abort signal to API call', () => {
|
|
const builderStore = useBuilderStore();
|
|
|
|
// Mock the API to prevent actual network calls
|
|
apiSpy.mockImplementationOnce(() => {});
|
|
|
|
builderStore.sendChatMessage({ text: 'test' });
|
|
|
|
// Verify the API was called with correct parameters
|
|
expect(apiSpy).toHaveBeenCalled();
|
|
const callArgs = apiSpy.mock.calls[0];
|
|
expect(callArgs).toHaveLength(6); // Should have 6 arguments
|
|
|
|
const signal = callArgs[5]; // The 6th argument is the abort signal
|
|
expect(signal).toBeDefined();
|
|
expect(signal).toBeInstanceOf(AbortSignal);
|
|
|
|
// Check that it's the same signal from the controller
|
|
const controller = builderStore.streamingAbortController;
|
|
expect(controller).not.toBeNull();
|
|
expect(controller).toBeInstanceOf(AbortController);
|
|
expect(signal).toBe(controller!.signal);
|
|
});
|
|
|
|
it('should not create error message for aborted requests', async () => {
|
|
const builderStore = useBuilderStore();
|
|
|
|
// Track telemetry calls
|
|
const telemetryTrackSpy = vi.fn();
|
|
track.mockImplementation(telemetryTrackSpy);
|
|
|
|
// Simulate abort error
|
|
const abortError = new Error('AbortError');
|
|
abortError.name = 'AbortError';
|
|
|
|
apiSpy.mockImplementationOnce((_ctx, _payload, _onMessage, _onDone, onError) => {
|
|
// Call error handler immediately
|
|
onError(abortError);
|
|
});
|
|
|
|
// Clear messages before test
|
|
builderStore.chatMessages.length = 0;
|
|
|
|
builderStore.sendChatMessage({ text: 'test' });
|
|
|
|
// Wait for the error to be processed
|
|
await vi.waitFor(() => expect(builderStore.chatMessages.length).toBeGreaterThan(1));
|
|
|
|
// Should not track error for abort
|
|
expect(telemetryTrackSpy).not.toHaveBeenCalledWith(
|
|
'Workflow generation errored',
|
|
expect.anything(),
|
|
);
|
|
|
|
// Find the assistant messages (skip user message)
|
|
const assistantMessages = builderStore.chatMessages.filter((msg) => msg.role === 'assistant');
|
|
expect(assistantMessages).toHaveLength(1);
|
|
expect(assistantMessages[0].type).toBe('text');
|
|
expect((assistantMessages[0] as ChatUI.TextMessage).content).toBe('[Task aborted]');
|
|
});
|
|
});
|
|
|
|
describe('Rating logic integration', () => {
|
|
it('should clear ratings from existing messages when preparing for streaming', () => {
|
|
const builderStore = useBuilderStore();
|
|
|
|
// Setup initial messages with ratings
|
|
builderStore.chatMessages = [
|
|
{
|
|
id: 'msg-1',
|
|
role: 'assistant',
|
|
type: 'text',
|
|
content: 'Previous message',
|
|
showRating: true,
|
|
ratingStyle: 'regular',
|
|
read: false,
|
|
} satisfies ChatUI.AssistantMessage,
|
|
{
|
|
id: 'msg-2',
|
|
role: 'assistant',
|
|
type: 'text',
|
|
content: 'Another message',
|
|
showRating: true,
|
|
ratingStyle: 'minimal',
|
|
read: false,
|
|
} satisfies ChatUI.AssistantMessage,
|
|
];
|
|
|
|
// Mock API to prevent actual network calls
|
|
apiSpy.mockImplementationOnce(() => {});
|
|
|
|
// Send new message which calls prepareForStreaming
|
|
builderStore.sendChatMessage({ text: 'New message' });
|
|
|
|
// Verify that existing messages no longer have rating properties
|
|
expect(builderStore.chatMessages).toHaveLength(3); // 2 existing + 1 new user message
|
|
|
|
const firstMessage = builderStore.chatMessages[0] as ChatUI.TextMessage;
|
|
expect(firstMessage).not.toHaveProperty('showRating');
|
|
expect(firstMessage).not.toHaveProperty('ratingStyle');
|
|
expect(firstMessage.content).toBe('Previous message');
|
|
|
|
const secondMessage = builderStore.chatMessages[1] as ChatUI.TextMessage;
|
|
expect(secondMessage).not.toHaveProperty('showRating');
|
|
expect(secondMessage).not.toHaveProperty('ratingStyle');
|
|
expect(secondMessage.content).toBe('Another message');
|
|
|
|
// New user message should not have rating properties
|
|
const userMessage = builderStore.chatMessages[2] as ChatUI.TextMessage;
|
|
expect(userMessage.role).toBe('user');
|
|
expect(userMessage.content).toBe('New message');
|
|
expect(userMessage).not.toHaveProperty('showRating');
|
|
expect(userMessage).not.toHaveProperty('ratingStyle');
|
|
});
|
|
});
|
|
});
|