@lobehub/chat
Version:
Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.
360 lines (281 loc) • 10.9 kB
text/typescript
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { NetworkConnectionError, PageNotFoundError, TimeoutError } from '../../utils/errorType';
import { tavily } from '../tavily';
// Mock dependencies
vi.mock('../../utils/withTimeout', () => ({
DEFAULT_TIMEOUT: 30000,
withTimeout: vi.fn(),
}));
describe('tavily crawler', () => {
beforeEach(() => {
vi.clearAllMocks();
delete process.env.TAVILY_API_KEY;
delete process.env.TAVILY_EXTRACT_DEPTH;
});
it('should successfully crawl content with API key', async () => {
process.env.TAVILY_API_KEY = 'test-api-key';
const mockResponse = {
ok: true,
json: vi.fn().mockResolvedValue({
base_url: 'https://api.tavily.com',
response_time: 1.5,
results: [
{
url: 'https://example.com',
raw_content:
'This is a test raw content with sufficient length to pass validation. '.repeat(3),
images: ['https://example.com/image1.jpg', 'https://example.com/image2.jpg'],
},
],
}),
};
const { withTimeout } = await import('../../utils/withTimeout');
vi.mocked(withTimeout).mockResolvedValue(mockResponse as any);
const result = await tavily('https://example.com', { filterOptions: {} });
expect(result).toEqual({
content: 'This is a test raw content with sufficient length to pass validation. '.repeat(3),
contentType: 'text',
length: 'This is a test raw content with sufficient length to pass validation. '.repeat(3)
.length,
siteName: 'example.com',
title: 'example.com',
url: 'https://example.com',
});
expect(withTimeout).toHaveBeenCalledWith(expect.any(Promise), 30000);
});
it('should use custom extract depth when provided', async () => {
process.env.TAVILY_API_KEY = 'test-api-key';
process.env.TAVILY_EXTRACT_DEPTH = 'advanced';
const mockResponse = {
ok: true,
json: vi.fn().mockResolvedValue({
base_url: 'https://api.tavily.com',
response_time: 2.1,
results: [
{
url: 'https://example.com',
raw_content: 'Advanced extraction content with more details. '.repeat(5),
},
],
}),
};
const { withTimeout } = await import('../../utils/withTimeout');
vi.mocked(withTimeout).mockResolvedValue(mockResponse as any);
await tavily('https://example.com', { filterOptions: {} });
expect(withTimeout).toHaveBeenCalledWith(expect.any(Promise), 30000);
});
it('should handle missing API key', async () => {
const mockResponse = {
ok: true,
json: vi.fn().mockResolvedValue({
base_url: 'https://api.tavily.com',
response_time: 1.2,
results: [
{
url: 'https://example.com',
raw_content: 'Test content with sufficient length. '.repeat(5),
},
],
}),
};
const { withTimeout } = await import('../../utils/withTimeout');
vi.mocked(withTimeout).mockResolvedValue(mockResponse as any);
await tavily('https://example.com', { filterOptions: {} });
expect(withTimeout).toHaveBeenCalledWith(expect.any(Promise), 30000);
});
it('should return undefined when no results are returned', async () => {
process.env.TAVILY_API_KEY = 'test-api-key';
const mockResponse = {
ok: true,
json: vi.fn().mockResolvedValue({
base_url: 'https://api.tavily.com',
response_time: 0.8,
results: [],
}),
};
const { withTimeout } = await import('../../utils/withTimeout');
vi.mocked(withTimeout).mockResolvedValue(mockResponse as any);
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
const result = await tavily('https://example.com', { filterOptions: {} });
expect(result).toBeUndefined();
expect(consoleSpy).toHaveBeenCalledWith(
'Tavily API returned no results for URL:',
'https://example.com',
);
consoleSpy.mockRestore();
});
it('should return undefined for short content', async () => {
process.env.TAVILY_API_KEY = 'test-api-key';
const mockResponse = {
ok: true,
json: vi.fn().mockResolvedValue({
base_url: 'https://api.tavily.com',
response_time: 1.1,
results: [
{
url: 'https://example.com',
raw_content: 'Short', // Content too short
},
],
}),
};
const { withTimeout } = await import('../../utils/withTimeout');
vi.mocked(withTimeout).mockResolvedValue(mockResponse as any);
const result = await tavily('https://example.com', { filterOptions: {} });
expect(result).toBeUndefined();
});
it('should return undefined when raw_content is missing', async () => {
process.env.TAVILY_API_KEY = 'test-api-key';
const mockResponse = {
ok: true,
json: vi.fn().mockResolvedValue({
base_url: 'https://api.tavily.com',
response_time: 1.0,
results: [
{
url: 'https://example.com',
// raw_content is missing
images: ['https://example.com/image.jpg'],
},
],
}),
};
const { withTimeout } = await import('../../utils/withTimeout');
vi.mocked(withTimeout).mockResolvedValue(mockResponse as any);
const result = await tavily('https://example.com', { filterOptions: {} });
expect(result).toBeUndefined();
});
it('should throw PageNotFoundError for 404 status', async () => {
process.env.TAVILY_API_KEY = 'test-api-key';
const mockResponse = {
ok: false,
status: 404,
statusText: 'Not Found',
};
const { withTimeout } = await import('../../utils/withTimeout');
vi.mocked(withTimeout).mockResolvedValue(mockResponse as any);
await expect(tavily('https://example.com', { filterOptions: {} })).rejects.toThrow(
PageNotFoundError,
);
});
it('should throw error for other HTTP errors', async () => {
process.env.TAVILY_API_KEY = 'test-api-key';
const mockResponse = {
ok: false,
status: 500,
statusText: 'Internal Server Error',
};
const { withTimeout } = await import('../../utils/withTimeout');
vi.mocked(withTimeout).mockResolvedValue(mockResponse as any);
await expect(tavily('https://example.com', { filterOptions: {} })).rejects.toThrow(
'Tavily request failed with status 500: Internal Server Error',
);
});
it('should throw NetworkConnectionError for fetch failures', async () => {
process.env.TAVILY_API_KEY = 'test-api-key';
const { withTimeout } = await import('../../utils/withTimeout');
vi.mocked(withTimeout).mockRejectedValue(new Error('fetch failed'));
await expect(tavily('https://example.com', { filterOptions: {} })).rejects.toThrow(
NetworkConnectionError,
);
});
it('should throw TimeoutError when request times out', async () => {
process.env.TAVILY_API_KEY = 'test-api-key';
const timeoutError = new TimeoutError('Request timeout');
const { withTimeout } = await import('../../utils/withTimeout');
vi.mocked(withTimeout).mockRejectedValue(timeoutError);
await expect(tavily('https://example.com', { filterOptions: {} })).rejects.toThrow(
TimeoutError,
);
});
it('should rethrow unknown errors', async () => {
process.env.TAVILY_API_KEY = 'test-api-key';
const unknownError = new Error('Unknown error');
const { withTimeout } = await import('../../utils/withTimeout');
vi.mocked(withTimeout).mockRejectedValue(unknownError);
await expect(tavily('https://example.com', { filterOptions: {} })).rejects.toThrow(
'Unknown error',
);
});
it('should return undefined when JSON parsing fails', async () => {
process.env.TAVILY_API_KEY = 'test-api-key';
const mockResponse = {
ok: true,
json: vi.fn().mockRejectedValue(new Error('Invalid JSON')),
};
const { withTimeout } = await import('../../utils/withTimeout');
vi.mocked(withTimeout).mockResolvedValue(mockResponse as any);
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const result = await tavily('https://example.com', { filterOptions: {} });
expect(result).toBeUndefined();
expect(consoleSpy).toHaveBeenCalled();
consoleSpy.mockRestore();
});
it('should use result URL when available', async () => {
process.env.TAVILY_API_KEY = 'test-api-key';
const mockResponse = {
ok: true,
json: vi.fn().mockResolvedValue({
base_url: 'https://api.tavily.com',
response_time: 1.3,
results: [
{
url: 'https://redirected.example.com',
raw_content: 'Test content with sufficient length. '.repeat(5),
},
],
}),
};
const { withTimeout } = await import('../../utils/withTimeout');
vi.mocked(withTimeout).mockResolvedValue(mockResponse as any);
const result = await tavily('https://example.com', { filterOptions: {} });
expect(result?.url).toBe('https://redirected.example.com');
});
it('should fallback to original URL when result URL is missing', async () => {
process.env.TAVILY_API_KEY = 'test-api-key';
const mockResponse = {
ok: true,
json: vi.fn().mockResolvedValue({
base_url: 'https://api.tavily.com',
response_time: 1.4,
results: [
{
raw_content: 'Test content with sufficient length. '.repeat(5),
// url is missing
},
],
}),
};
const { withTimeout } = await import('../../utils/withTimeout');
vi.mocked(withTimeout).mockResolvedValue(mockResponse as any);
const result = await tavily('https://example.com', { filterOptions: {} });
expect(result?.url).toBe('https://example.com');
});
it('should handle failed results in response', async () => {
process.env.TAVILY_API_KEY = 'test-api-key';
const mockResponse = {
ok: true,
json: vi.fn().mockResolvedValue({
base_url: 'https://api.tavily.com',
response_time: 1.6,
results: [],
failed_results: [
{
url: 'https://example.com',
error: 'Page not accessible',
},
],
}),
};
const { withTimeout } = await import('../../utils/withTimeout');
vi.mocked(withTimeout).mockResolvedValue(mockResponse as any);
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
const result = await tavily('https://example.com', { filterOptions: {} });
expect(result).toBeUndefined();
expect(consoleSpy).toHaveBeenCalledWith(
'Tavily API returned no results for URL:',
'https://example.com',
);
consoleSpy.mockRestore();
});
});