first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { postWithFiles } from '@n8n/chat/api/generic';
|
||||
|
||||
describe('postWithFiles', () => {
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should properly serialize object metadata to JSON string in FormData', async () => {
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => await Promise.resolve({ success: true }),
|
||||
text: async () => await Promise.resolve('success'),
|
||||
clone: () => mockResponse,
|
||||
} as Response;
|
||||
|
||||
const fetchSpy = vi.spyOn(global, 'fetch').mockResolvedValue(mockResponse);
|
||||
|
||||
const testFile = new File(['test content'], 'test.txt', { type: 'text/plain' });
|
||||
const metadata = {
|
||||
userId: 'user-123',
|
||||
token: 'abc-def-ghi',
|
||||
nested: {
|
||||
prop: 'value',
|
||||
num: 42,
|
||||
},
|
||||
};
|
||||
|
||||
await postWithFiles(
|
||||
'https://example.com/webhook',
|
||||
{
|
||||
action: 'sendMessage',
|
||||
sessionId: 'test-session',
|
||||
chatInput: 'test message',
|
||||
metadata,
|
||||
},
|
||||
[testFile],
|
||||
);
|
||||
|
||||
expect(fetchSpy).toHaveBeenCalledWith('https://example.com/webhook', {
|
||||
method: 'POST',
|
||||
body: expect.any(FormData),
|
||||
mode: 'cors',
|
||||
cache: 'no-cache',
|
||||
headers: {},
|
||||
});
|
||||
|
||||
// Get the FormData from the call
|
||||
const formData = fetchSpy.mock.calls[0][1]?.body as FormData;
|
||||
expect(formData).toBeInstanceOf(FormData);
|
||||
|
||||
// Verify that metadata was properly serialized as JSON, not "[object Object]"
|
||||
const metadataValue = formData.get('metadata');
|
||||
expect(metadataValue).toBe(JSON.stringify(metadata));
|
||||
|
||||
// Verify other fields are still strings
|
||||
expect(formData.get('action')).toBe('sendMessage');
|
||||
expect(formData.get('sessionId')).toBe('test-session');
|
||||
expect(formData.get('chatInput')).toBe('test message');
|
||||
|
||||
// Verify file was included
|
||||
expect(formData.get('files')).toBe(testFile);
|
||||
});
|
||||
|
||||
it('should handle primitive values correctly', async () => {
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => await Promise.resolve({ success: true }),
|
||||
text: async () => await Promise.resolve('success'),
|
||||
clone: () => mockResponse,
|
||||
} as Response;
|
||||
|
||||
const fetchSpy = vi.spyOn(global, 'fetch').mockResolvedValue(mockResponse);
|
||||
|
||||
await postWithFiles('https://example.com/webhook', {
|
||||
stringValue: 'test',
|
||||
});
|
||||
|
||||
const formData = fetchSpy.mock.calls[0][1]?.body as FormData;
|
||||
|
||||
expect(formData.get('stringValue')).toBe('test');
|
||||
});
|
||||
|
||||
it('should handle arrays as JSON strings', async () => {
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => await Promise.resolve({ success: true }),
|
||||
text: async () => await Promise.resolve('success'),
|
||||
clone: () => mockResponse,
|
||||
} as Response;
|
||||
|
||||
const fetchSpy = vi.spyOn(global, 'fetch').mockResolvedValue(mockResponse);
|
||||
|
||||
const arrayValue = ['item1', 'item2', { nested: 'object' }];
|
||||
|
||||
await postWithFiles('https://example.com/webhook', {
|
||||
arrayValue,
|
||||
});
|
||||
|
||||
const formData = fetchSpy.mock.calls[0][1]?.body as FormData;
|
||||
expect(formData.get('arrayValue')).toBe(JSON.stringify(arrayValue));
|
||||
});
|
||||
|
||||
it('should handle empty objects correctly', async () => {
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => await Promise.resolve({ success: true }),
|
||||
text: async () => await Promise.resolve('success'),
|
||||
clone: () => mockResponse,
|
||||
} as Response;
|
||||
|
||||
const fetchSpy = vi.spyOn(global, 'fetch').mockResolvedValue(mockResponse);
|
||||
|
||||
await postWithFiles('https://example.com/webhook', {
|
||||
emptyObject: {},
|
||||
});
|
||||
|
||||
const formData = fetchSpy.mock.calls[0][1]?.body as FormData;
|
||||
expect(formData.get('emptyObject')).toBe('{}');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,623 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { sendMessageStreaming } from '@n8n/chat/api';
|
||||
import type { ChatOptions } from '@n8n/chat/types';
|
||||
|
||||
describe('sendMessageStreaming', () => {
|
||||
const mockOptions: ChatOptions = {
|
||||
webhookUrl: 'https://test.example.com/webhook',
|
||||
chatSessionKey: 'sessionId',
|
||||
chatInputKey: 'chatInput',
|
||||
i18n: {
|
||||
en: {
|
||||
title: 'Test',
|
||||
subtitle: 'Test',
|
||||
footer: 'Test',
|
||||
getStarted: 'Test',
|
||||
inputPlaceholder: 'Test',
|
||||
closeButtonTooltip: 'Test',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should call the webhook URL with correct parameters', async () => {
|
||||
const chunks = [
|
||||
{
|
||||
type: 'begin',
|
||||
metadata: {
|
||||
nodeId: 'node-1',
|
||||
nodeName: 'Test Node',
|
||||
timestamp: Date.now(),
|
||||
runIndex: 0,
|
||||
itemIndex: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
content: 'Hello ',
|
||||
metadata: {
|
||||
nodeId: 'node-1',
|
||||
nodeName: 'Test Node',
|
||||
timestamp: Date.now(),
|
||||
runIndex: 0,
|
||||
itemIndex: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
content: 'World!',
|
||||
metadata: {
|
||||
nodeId: 'node-1',
|
||||
nodeName: 'Test Node',
|
||||
timestamp: Date.now(),
|
||||
runIndex: 0,
|
||||
itemIndex: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'end',
|
||||
metadata: {
|
||||
nodeId: 'node-1',
|
||||
nodeName: 'Test Node',
|
||||
timestamp: Date.now(),
|
||||
runIndex: 0,
|
||||
itemIndex: 0,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
chunks.forEach((chunk) => {
|
||||
const data = JSON.stringify(chunk) + '\n';
|
||||
controller.enqueue(encoder.encode(data));
|
||||
});
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
body: stream,
|
||||
headers: new Headers(),
|
||||
} as Response;
|
||||
|
||||
vi.spyOn(global, 'fetch').mockResolvedValue(mockResponse);
|
||||
|
||||
const onChunk = vi.fn();
|
||||
const onBeginMessage = vi.fn();
|
||||
const onEndMessage = vi.fn();
|
||||
|
||||
await sendMessageStreaming('Test message', [], 'test-session-id', mockOptions, {
|
||||
onChunk,
|
||||
onBeginMessage,
|
||||
onEndMessage,
|
||||
});
|
||||
|
||||
expect(fetch).toHaveBeenCalledWith('https://test.example.com/webhook', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'text/plain',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: 'sendMessage',
|
||||
sessionId: 'test-session-id',
|
||||
chatInput: 'Test message',
|
||||
}),
|
||||
});
|
||||
|
||||
expect(onBeginMessage).toHaveBeenCalledTimes(1);
|
||||
expect(onBeginMessage).toHaveBeenCalledWith('node-1', 0);
|
||||
expect(onChunk).toHaveBeenCalledTimes(2);
|
||||
expect(onChunk).toHaveBeenCalledWith('Hello ', 'node-1', 0);
|
||||
expect(onChunk).toHaveBeenCalledWith('World!', 'node-1', 0);
|
||||
expect(onEndMessage).toHaveBeenCalledTimes(1);
|
||||
expect(onEndMessage).toHaveBeenCalledWith('node-1', 0);
|
||||
});
|
||||
|
||||
it('should handle multiple runs and items correctly', async () => {
|
||||
const chunks = [
|
||||
{
|
||||
type: 'begin',
|
||||
metadata: {
|
||||
nodeId: 'node-1',
|
||||
nodeName: 'Test Node',
|
||||
timestamp: Date.now(),
|
||||
runIndex: 0,
|
||||
itemIndex: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
content: 'Run 0 Item 0 ',
|
||||
metadata: {
|
||||
nodeId: 'node-1',
|
||||
nodeName: 'Test Node',
|
||||
timestamp: Date.now(),
|
||||
runIndex: 0,
|
||||
itemIndex: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'end',
|
||||
metadata: {
|
||||
nodeId: 'node-1',
|
||||
nodeName: 'Test Node',
|
||||
timestamp: Date.now(),
|
||||
runIndex: 0,
|
||||
itemIndex: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'begin',
|
||||
metadata: {
|
||||
nodeId: 'node-1',
|
||||
nodeName: 'Test Node',
|
||||
timestamp: Date.now(),
|
||||
runIndex: 1,
|
||||
itemIndex: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
content: 'Run 1 Item 0 ',
|
||||
metadata: {
|
||||
nodeId: 'node-1',
|
||||
nodeName: 'Test Node',
|
||||
timestamp: Date.now(),
|
||||
runIndex: 1,
|
||||
itemIndex: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'end',
|
||||
metadata: {
|
||||
nodeId: 'node-1',
|
||||
nodeName: 'Test Node',
|
||||
timestamp: Date.now(),
|
||||
runIndex: 1,
|
||||
itemIndex: 0,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
chunks.forEach((chunk) => {
|
||||
const data = JSON.stringify(chunk) + '\n';
|
||||
controller.enqueue(encoder.encode(data));
|
||||
});
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
body: stream,
|
||||
headers: new Headers(),
|
||||
} as Response;
|
||||
|
||||
vi.spyOn(global, 'fetch').mockResolvedValue(mockResponse);
|
||||
|
||||
const onChunk = vi.fn();
|
||||
const onBeginMessage = vi.fn();
|
||||
const onEndMessage = vi.fn();
|
||||
|
||||
await sendMessageStreaming('Test message', [], 'test-session-id', mockOptions, {
|
||||
onChunk,
|
||||
onBeginMessage,
|
||||
onEndMessage,
|
||||
});
|
||||
|
||||
expect(onBeginMessage).toHaveBeenCalledTimes(2);
|
||||
expect(onBeginMessage).toHaveBeenCalledWith('node-1', 0);
|
||||
expect(onBeginMessage).toHaveBeenCalledWith('node-1', 1);
|
||||
expect(onChunk).toHaveBeenCalledTimes(2);
|
||||
expect(onChunk).toHaveBeenCalledWith('Run 0 Item 0 ', 'node-1', 0);
|
||||
expect(onChunk).toHaveBeenCalledWith('Run 1 Item 0 ', 'node-1', 1);
|
||||
expect(onEndMessage).toHaveBeenCalledTimes(2);
|
||||
expect(onEndMessage).toHaveBeenCalledWith('node-1', 0);
|
||||
expect(onEndMessage).toHaveBeenCalledWith('node-1', 1);
|
||||
});
|
||||
|
||||
it('should support file uploads with streaming', async () => {
|
||||
const testFile = new File(['test'], 'test.txt', { type: 'text/plain' });
|
||||
|
||||
const chunks = [
|
||||
{
|
||||
type: 'begin',
|
||||
metadata: {
|
||||
nodeId: 'node-1',
|
||||
nodeName: 'Test Node',
|
||||
timestamp: Date.now(),
|
||||
runIndex: 0,
|
||||
itemIndex: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
content: 'File processed: ',
|
||||
metadata: {
|
||||
nodeId: 'node-1',
|
||||
nodeName: 'Test Node',
|
||||
timestamp: Date.now(),
|
||||
runIndex: 0,
|
||||
itemIndex: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'item',
|
||||
content: 'test.txt',
|
||||
metadata: {
|
||||
nodeId: 'node-1',
|
||||
nodeName: 'Test Node',
|
||||
timestamp: Date.now(),
|
||||
runIndex: 0,
|
||||
itemIndex: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'end',
|
||||
metadata: {
|
||||
nodeId: 'node-1',
|
||||
nodeName: 'Test Node',
|
||||
timestamp: Date.now(),
|
||||
runIndex: 0,
|
||||
itemIndex: 0,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
chunks.forEach((chunk) => {
|
||||
const data = JSON.stringify(chunk) + '\n';
|
||||
controller.enqueue(encoder.encode(data));
|
||||
});
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
body: stream,
|
||||
headers: new Headers(),
|
||||
} as Response;
|
||||
|
||||
vi.spyOn(global, 'fetch').mockResolvedValue(mockResponse);
|
||||
|
||||
const onChunk = vi.fn();
|
||||
const onBeginMessage = vi.fn();
|
||||
const onEndMessage = vi.fn();
|
||||
|
||||
await sendMessageStreaming('Test message', [testFile], 'test-session-id', mockOptions, {
|
||||
onChunk,
|
||||
onBeginMessage,
|
||||
onEndMessage,
|
||||
});
|
||||
|
||||
// Verify FormData was used for file upload
|
||||
expect(fetch).toHaveBeenCalledWith('https://test.example.com/webhook', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'text/plain',
|
||||
},
|
||||
body: expect.any(FormData),
|
||||
});
|
||||
|
||||
expect(onBeginMessage).toHaveBeenCalledTimes(1);
|
||||
expect(onBeginMessage).toHaveBeenCalledWith('node-1', 0);
|
||||
expect(onChunk).toHaveBeenCalledTimes(2);
|
||||
expect(onChunk).toHaveBeenCalledWith('File processed: ', 'node-1', 0);
|
||||
expect(onChunk).toHaveBeenCalledWith('test.txt', 'node-1', 0);
|
||||
expect(onEndMessage).toHaveBeenCalledTimes(1);
|
||||
expect(onEndMessage).toHaveBeenCalledWith('node-1', 0);
|
||||
});
|
||||
|
||||
it('should strip Content-Type header when uploading files even if set in webhookConfig', async () => {
|
||||
const optionsWithContentType: ChatOptions = {
|
||||
...mockOptions,
|
||||
webhookConfig: {
|
||||
headers: { 'Content-Type': 'application/json', 'X-Custom': 'value' },
|
||||
},
|
||||
};
|
||||
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
body: new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(new TextEncoder().encode('{"type":"end"}\n'));
|
||||
controller.close();
|
||||
},
|
||||
}),
|
||||
headers: new Headers(),
|
||||
} as Response;
|
||||
|
||||
vi.spyOn(global, 'fetch').mockResolvedValue(mockResponse);
|
||||
|
||||
await sendMessageStreaming(
|
||||
'test',
|
||||
[new File([''], 'test.txt')],
|
||||
'session',
|
||||
optionsWithContentType,
|
||||
{
|
||||
onChunk: vi.fn(),
|
||||
onEndMessage: vi.fn(),
|
||||
onBeginMessage: vi.fn(),
|
||||
},
|
||||
);
|
||||
|
||||
// Content-Type must be excluded for FormData (browser sets it with boundary)
|
||||
// Other custom headers should still be included
|
||||
expect(fetch).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
expect.objectContaining({
|
||||
headers: { Accept: 'text/plain', 'X-Custom': 'value' },
|
||||
body: expect.any(FormData),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle HTTP errors', async () => {
|
||||
const mockResponse = {
|
||||
ok: false,
|
||||
status: 500,
|
||||
headers: new Headers(),
|
||||
text: async () => 'Internal Server Error',
|
||||
} as Response;
|
||||
|
||||
vi.spyOn(global, 'fetch').mockResolvedValue(mockResponse);
|
||||
|
||||
await expect(
|
||||
sendMessageStreaming('Test message', [], 'test-session-id', mockOptions, {
|
||||
onChunk: vi.fn(),
|
||||
onEndMessage: vi.fn(),
|
||||
onBeginMessage: vi.fn(),
|
||||
}),
|
||||
).rejects.toThrow('Error while sending message. Error: Internal Server Error');
|
||||
});
|
||||
|
||||
it('should handle missing response body', async () => {
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
body: null,
|
||||
headers: new Headers(),
|
||||
} as Response;
|
||||
|
||||
vi.spyOn(global, 'fetch').mockResolvedValue(mockResponse);
|
||||
|
||||
await expect(
|
||||
sendMessageStreaming('Test message', [], 'test-session-id', mockOptions, {
|
||||
onChunk: vi.fn(),
|
||||
onEndMessage: vi.fn(),
|
||||
onBeginMessage: vi.fn(),
|
||||
}),
|
||||
).rejects.toThrow('Response body is not readable');
|
||||
});
|
||||
|
||||
it('should include custom headers from webhook config', async () => {
|
||||
const optionsWithHeaders: ChatOptions = {
|
||||
...mockOptions,
|
||||
webhookConfig: {
|
||||
headers: {
|
||||
Authorization: 'Bearer token',
|
||||
'X-Custom-Header': 'value',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const chunks = [
|
||||
{
|
||||
type: 'begin',
|
||||
metadata: { nodeId: 'node-1', nodeName: 'Test Node', timestamp: Date.now() },
|
||||
},
|
||||
{ type: 'end', metadata: { nodeId: 'node-1', nodeName: 'Test Node', timestamp: Date.now() } },
|
||||
];
|
||||
const encoder = new TextEncoder();
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
chunks.forEach((chunk) => {
|
||||
const data = JSON.stringify(chunk) + '\n';
|
||||
controller.enqueue(encoder.encode(data));
|
||||
});
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
body: stream,
|
||||
headers: new Headers(),
|
||||
} as Response;
|
||||
|
||||
vi.spyOn(global, 'fetch').mockResolvedValue(mockResponse);
|
||||
|
||||
await sendMessageStreaming('Test message', [], 'test-session-id', optionsWithHeaders, {
|
||||
onChunk: vi.fn(),
|
||||
onEndMessage: vi.fn(),
|
||||
onBeginMessage: vi.fn(),
|
||||
});
|
||||
|
||||
expect(fetch).toHaveBeenCalledWith('https://test.example.com/webhook', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'text/plain',
|
||||
Authorization: 'Bearer token',
|
||||
'X-Custom-Header': 'value',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: 'sendMessage',
|
||||
sessionId: 'test-session-id',
|
||||
chatInput: 'Test message',
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it('should include metadata when provided', async () => {
|
||||
const optionsWithMetadata: ChatOptions = {
|
||||
...mockOptions,
|
||||
metadata: {
|
||||
userId: 'user-123',
|
||||
source: 'chat-widget',
|
||||
},
|
||||
};
|
||||
|
||||
const chunks = [
|
||||
{
|
||||
type: 'begin',
|
||||
metadata: { nodeId: 'node-1', nodeName: 'Test Node', timestamp: Date.now() },
|
||||
},
|
||||
{ type: 'end', metadata: { nodeId: 'node-1', nodeName: 'Test Node', timestamp: Date.now() } },
|
||||
];
|
||||
const encoder = new TextEncoder();
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
chunks.forEach((chunk) => {
|
||||
const data = JSON.stringify(chunk) + '\n';
|
||||
controller.enqueue(encoder.encode(data));
|
||||
});
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
body: stream,
|
||||
headers: new Headers(),
|
||||
} as Response;
|
||||
|
||||
vi.spyOn(global, 'fetch').mockResolvedValue(mockResponse);
|
||||
|
||||
await sendMessageStreaming('Test message', [], 'test-session-id', optionsWithMetadata, {
|
||||
onChunk: vi.fn(),
|
||||
onEndMessage: vi.fn(),
|
||||
onBeginMessage: vi.fn(),
|
||||
});
|
||||
|
||||
expect(fetch).toHaveBeenCalledWith('https://test.example.com/webhook', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'text/plain',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: 'sendMessage',
|
||||
sessionId: 'test-session-id',
|
||||
chatInput: 'Test message',
|
||||
metadata: {
|
||||
userId: 'user-123',
|
||||
source: 'chat-widget',
|
||||
},
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
describe('async handlers', () => {
|
||||
it('should support async onEndMessage handler', async () => {
|
||||
const onEndMessage = vi.fn().mockImplementation(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
});
|
||||
|
||||
const chunks = [
|
||||
{
|
||||
type: 'begin',
|
||||
metadata: { nodeId: 'node-1', nodeName: 'Test Node', timestamp: Date.now() },
|
||||
},
|
||||
{
|
||||
type: 'end',
|
||||
metadata: { nodeId: 'node-1', nodeName: 'Test Node', timestamp: Date.now() },
|
||||
},
|
||||
];
|
||||
const encoder = new TextEncoder();
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
chunks.forEach((chunk) => {
|
||||
const data = JSON.stringify(chunk) + '\n';
|
||||
controller.enqueue(encoder.encode(data));
|
||||
});
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
body: stream,
|
||||
headers: new Headers(),
|
||||
} as Response;
|
||||
|
||||
vi.spyOn(global, 'fetch').mockResolvedValue(mockResponse);
|
||||
|
||||
await sendMessageStreaming('Test message', [], 'test-session-id', mockOptions, {
|
||||
onChunk: vi.fn(),
|
||||
onEndMessage,
|
||||
onBeginMessage: vi.fn(),
|
||||
});
|
||||
|
||||
expect(onEndMessage).toHaveBeenCalledWith('node-1', undefined);
|
||||
});
|
||||
|
||||
it('should await async onEndMessage on error chunks', async () => {
|
||||
const onEndMessage = vi.fn().mockImplementation(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
});
|
||||
|
||||
const chunks = [
|
||||
{
|
||||
type: 'begin',
|
||||
metadata: { nodeId: 'node-1', nodeName: 'Test Node', timestamp: Date.now() },
|
||||
},
|
||||
{
|
||||
type: 'error',
|
||||
content: 'Something went wrong',
|
||||
metadata: { nodeId: 'node-1', nodeName: 'Test Node', timestamp: Date.now() },
|
||||
},
|
||||
];
|
||||
const encoder = new TextEncoder();
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
chunks.forEach((chunk) => {
|
||||
const data = JSON.stringify(chunk) + '\n';
|
||||
controller.enqueue(encoder.encode(data));
|
||||
});
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
|
||||
const mockResponse = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
body: stream,
|
||||
headers: new Headers(),
|
||||
} as Response;
|
||||
|
||||
vi.spyOn(global, 'fetch').mockResolvedValue(mockResponse);
|
||||
|
||||
const onChunk = vi.fn();
|
||||
await sendMessageStreaming('Test message', [], 'test-session-id', mockOptions, {
|
||||
onChunk,
|
||||
onEndMessage,
|
||||
onBeginMessage: vi.fn(),
|
||||
});
|
||||
|
||||
expect(onChunk).toHaveBeenCalledWith('Error: Something went wrong', 'node-1', undefined);
|
||||
expect(onEndMessage).toHaveBeenCalledWith('node-1', undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user