mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 08:50:15 +01:00
* 🪦 refactor: Remove Legacy Code (#10533) * 🗑️ chore: Remove unused Legacy Provider clients and related helpers * Deleted OpenAIClient and GoogleClient files along with their associated tests. * Removed references to these clients in the clients index file. * Cleaned up typedefs by removing the OpenAISpecClient export. * Updated chat controllers to use the OpenAI SDK directly instead of the removed client classes. * chore/remove-openapi-specs * 🗑️ chore: Remove unused mergeSort and misc utility functions * Deleted mergeSort.js and misc.js files as they are no longer needed. * Removed references to cleanUpPrimaryKeyValue in messages.js and adjusted related logic. * Updated mongoMeili.ts to eliminate local implementations of removed functions. * chore: remove legacy endpoints * chore: remove all plugins endpoint related code * chore: remove unused prompt handling code and clean up imports * Deleted handleInputs.js and instructions.js files as they are no longer needed. * Removed references to these files in the prompts index.js. * Updated docker-compose.yml to simplify reverse proxy configuration. * chore: remove unused LightningIcon import from Icons.tsx * chore: clean up translation.json by removing deprecated and unused keys * chore: update Jest configuration and remove unused mock file * Simplified the setupFiles array in jest.config.js by removing the fetchEventSource mock. * Deleted the fetchEventSource.js mock file as it is no longer needed. * fix: simplify endpoint type check in Landing and ConversationStarters components * Updated the endpoint type check to use strict equality for better clarity and performance. * Ensured consistency in the handling of the azureOpenAI endpoint across both components. * chore: remove unused dependencies from package.json and package-lock.json * chore: remove legacy EditController, associated routes and imports * chore: update banResponse logic to refine request handling for banned users * chore: remove unused validateEndpoint middleware and its references * chore: remove unused 'res' parameter from initializeClient in multiple endpoint files * chore: remove unused 'isSmallScreen' prop from BookmarkNav and NewChat components; clean up imports in ArchivedChatsTable and useSetIndexOptions hooks; enhance localization in PromptVersions * chore: remove unused import of Constants and TMessage from MobileNav; retain only necessary QueryKeys import * chore: remove unused TResPlugin type and related references; clean up imports in types and schemas * 🪦 refactor: Remove Legacy Code (#10533) * 🗑️ chore: Remove unused Legacy Provider clients and related helpers * Deleted OpenAIClient and GoogleClient files along with their associated tests. * Removed references to these clients in the clients index file. * Cleaned up typedefs by removing the OpenAISpecClient export. * Updated chat controllers to use the OpenAI SDK directly instead of the removed client classes. * chore/remove-openapi-specs * 🗑️ chore: Remove unused mergeSort and misc utility functions * Deleted mergeSort.js and misc.js files as they are no longer needed. * Removed references to cleanUpPrimaryKeyValue in messages.js and adjusted related logic. * Updated mongoMeili.ts to eliminate local implementations of removed functions. * chore: remove legacy endpoints * chore: remove all plugins endpoint related code * chore: remove unused prompt handling code and clean up imports * Deleted handleInputs.js and instructions.js files as they are no longer needed. * Removed references to these files in the prompts index.js. * Updated docker-compose.yml to simplify reverse proxy configuration. * chore: remove unused LightningIcon import from Icons.tsx * chore: clean up translation.json by removing deprecated and unused keys * chore: update Jest configuration and remove unused mock file * Simplified the setupFiles array in jest.config.js by removing the fetchEventSource mock. * Deleted the fetchEventSource.js mock file as it is no longer needed. * fix: simplify endpoint type check in Landing and ConversationStarters components * Updated the endpoint type check to use strict equality for better clarity and performance. * Ensured consistency in the handling of the azureOpenAI endpoint across both components. * chore: remove unused dependencies from package.json and package-lock.json * chore: remove legacy EditController, associated routes and imports * chore: update banResponse logic to refine request handling for banned users * chore: remove unused validateEndpoint middleware and its references * chore: remove unused 'res' parameter from initializeClient in multiple endpoint files * chore: remove unused 'isSmallScreen' prop from BookmarkNav and NewChat components; clean up imports in ArchivedChatsTable and useSetIndexOptions hooks; enhance localization in PromptVersions * chore: remove unused import of Constants and TMessage from MobileNav; retain only necessary QueryKeys import * chore: remove unused TResPlugin type and related references; clean up imports in types and schemas * 📦 chore: Bump Express.js to v5 (#10671) * chore: update express to version 5.1.0 in package.json * chore: update express-rate-limit to version 8.2.1 in package.json and package-lock.json * fix: Enhance server startup error handling in experimental and index files * Added error handling for server startup in both experimental.js and index.js to log errors and exit the process if the server fails to start. * Updated comments in openidStrategy.js to clarify the purpose of the CustomOpenIDStrategy class and its relation to Express version changes. * chore: Implement rate limiting for all POST routes excluding /speech, required for express v5 * Added middleware to apply IP and user rate limiters to all POST requests, ensuring that the /speech route remains unaffected. * Enhanced code clarity with comments explaining the new rate limiting logic. * chore: Enable writable req.query for mongoSanitize compatibility in Express 5 * chore: Ensure req.body exists in multiple middleware and route files for Express 5 compatibility * 🗣 feat: MCP Status Accessibility Improvements (#10738) * feat: make MultiSelect highlight same opacity as other focus highlights in app * feat: add better screenreader announcements for mcp server and variable states * feat: memoize fullTitle calculation * 🪨 feat: Add PROXY support for AWS Bedrock endpoints (#8871) * feat: added PROXY support for AWS Bedrock endpoint * chore: explicit install of new packages required for bedrock proxy --------- Co-authored-by: Danny Avila <danny@librechat.ai> * ✨ feat: Implement Favorites functionality with controllers, hooks, and UI components * ✨ feat: Refactor Favorites functionality to support new data structure and enhance UI interactions * ✨ feat: Add endpoint to new conversation for agent favorites * ✨ feat: Enhance Conversations and Favorites components with expanded functionality and improved UI interactions * ✨ feat: Remove 'Pinned' label from UI translations for cleaner interface * feat: clean up comments and improve code readability in favorites and agent components; bump @librechat/data-schemas to 0.0.24 * ✨ feat: Enhance favorites management with validation, update data structure, and improve UI interactions * ✨ feat: Simplify rendering logic in EndpointModelItem and optimize useEffect dependencies in Conversations component * ✨ test: Update favorites mock implementation and improve button focus styles in AgentDetail tests * ✨ feat: Enhance favorites management by adding loading and error states, and refactor related hooks and components * ✨ feat: Add loading skeletons for favorites while agents are being fetched * ✨ feat: Improve loading experience in FavoritesList by adding skeleton placeholders for favorites and marketplace * feat: Optimize cache handling in Conversations and enhance FavoritesList to notify height changes on loading completion * ✨ feat: Add loading skeleton for SearchBar in Nav component and update agent avatar fallback icon to Feather * feat: Refactor FavoritesController validation, streamline ModelSelector component, and enhance EndpointModelItem with selection state * feat: Adjust padding in Conversations and FavoritesList components for improved layout consistency * feat: Refactor FavoritesController to use model methods for user updates and retrieval * feat: Enhance Favorites functionality with validation, cleanup, and improved error handling * tests: Update AgentCard and agent utilities to use Feather icon fallback instead of Bot icon * refactor: Remove collapsible animation styles from CSS * feat: Migrate favorites state management from Recoil to Jotai * fix: Correct type definition in useGetFavoritesQuery and ensure useFavorites is exported * refactor: Simplify AuthField component by removing TooltipAnchor and directly rendering Label * fix: Ensure favorites are always an array and update references in FavoritesList * style: Update Conversation component styles for improved UI consistency * feat: re-integrate AuthContext to manage agent marketplace visibility based on authentication state * fix: Improve optimistic updates in favorites mutation handling * feat: Implement error handling for favorites limit and consolidate marketplace access logic * fix: package-lock --------- Co-authored-by: Danny Avila <danny@librechat.ai> Co-authored-by: Dustin Healy <54083382+dustinhealy@users.noreply.github.com> Co-authored-by: Arthur Barrett <abarrett@fas.harvard.edu>
280 lines
8.7 KiB
TypeScript
280 lines
8.7 KiB
TypeScript
import React from 'react';
|
|
import { render, screen, fireEvent } from '@testing-library/react';
|
|
import '@testing-library/jest-dom';
|
|
import AgentCard from '../AgentCard';
|
|
import type t from 'librechat-data-provider';
|
|
|
|
// Mock useLocalize hook
|
|
jest.mock('~/hooks/useLocalize', () => () => (key: string) => {
|
|
const mockTranslations: Record<string, string> = {
|
|
com_agents_created_by: 'Created by',
|
|
com_agents_agent_card_label: '{{name}} agent. {{description}}',
|
|
com_agents_category_general: 'General',
|
|
com_agents_category_hr: 'Human Resources',
|
|
};
|
|
return mockTranslations[key] || key;
|
|
});
|
|
|
|
// Mock useAgentCategories hook
|
|
jest.mock('~/hooks', () => ({
|
|
useLocalize: () => (key: string, values?: Record<string, string>) => {
|
|
const mockTranslations: Record<string, string> = {
|
|
com_agents_created_by: 'Created by',
|
|
com_agents_agent_card_label: '{{name}} agent. {{description}}',
|
|
com_agents_category_general: 'General',
|
|
com_agents_category_hr: 'Human Resources',
|
|
};
|
|
let translation = mockTranslations[key] || key;
|
|
|
|
// Replace placeholders with actual values
|
|
if (values) {
|
|
Object.entries(values).forEach(([placeholder, value]) => {
|
|
translation = translation.replace(new RegExp(`{{${placeholder}}}`, 'g'), value);
|
|
});
|
|
}
|
|
|
|
return translation;
|
|
},
|
|
useAgentCategories: () => ({
|
|
categories: [
|
|
{ value: 'general', label: 'com_agents_category_general' },
|
|
{ value: 'hr', label: 'com_agents_category_hr' },
|
|
{ value: 'custom', label: 'Custom Category' }, // Non-localized custom category
|
|
],
|
|
}),
|
|
}));
|
|
|
|
describe('AgentCard', () => {
|
|
const mockAgent: t.Agent = {
|
|
id: '1',
|
|
name: 'Test Agent',
|
|
description: 'A test agent for testing purposes',
|
|
support_contact: {
|
|
name: 'Test Support',
|
|
email: 'test@example.com',
|
|
},
|
|
avatar: { filepath: '/test-avatar.png', source: 'local' },
|
|
created_at: 1672531200000,
|
|
instructions: 'Test instructions',
|
|
provider: 'openai' as const,
|
|
model: 'gpt-4',
|
|
model_parameters: {
|
|
temperature: 0.7,
|
|
maxContextTokens: 4096,
|
|
max_context_tokens: 4096,
|
|
max_output_tokens: 1024,
|
|
top_p: 1,
|
|
frequency_penalty: 0,
|
|
presence_penalty: 0,
|
|
},
|
|
};
|
|
|
|
const mockOnClick = jest.fn();
|
|
|
|
beforeEach(() => {
|
|
mockOnClick.mockClear();
|
|
});
|
|
|
|
it('renders agent information correctly', () => {
|
|
render(<AgentCard agent={mockAgent} onClick={mockOnClick} />);
|
|
|
|
expect(screen.getByText('Test Agent')).toBeInTheDocument();
|
|
expect(screen.getByText('A test agent for testing purposes')).toBeInTheDocument();
|
|
expect(screen.getByText('Test Support')).toBeInTheDocument();
|
|
});
|
|
|
|
it('displays avatar when provided as object', () => {
|
|
render(<AgentCard agent={mockAgent} onClick={mockOnClick} />);
|
|
|
|
const avatarImg = screen.getByAltText('Test Agent avatar');
|
|
expect(avatarImg).toBeInTheDocument();
|
|
expect(avatarImg).toHaveAttribute('src', '/test-avatar.png');
|
|
});
|
|
|
|
it('displays avatar when provided as string', () => {
|
|
const agentWithStringAvatar = {
|
|
...mockAgent,
|
|
avatar: '/string-avatar.png' as any, // Legacy support for string avatars
|
|
};
|
|
|
|
render(<AgentCard agent={agentWithStringAvatar} onClick={mockOnClick} />);
|
|
|
|
const avatarImg = screen.getByAltText('Test Agent avatar');
|
|
expect(avatarImg).toBeInTheDocument();
|
|
expect(avatarImg).toHaveAttribute('src', '/string-avatar.png');
|
|
});
|
|
|
|
it('displays Feather icon fallback when no avatar is provided', () => {
|
|
const agentWithoutAvatar = {
|
|
...mockAgent,
|
|
avatar: undefined,
|
|
};
|
|
|
|
render(<AgentCard agent={agentWithoutAvatar as any as t.Agent} onClick={mockOnClick} />);
|
|
|
|
// Check for Feather icon presence by looking for the svg with lucide-feather class
|
|
const featherIcon = document.querySelector('.lucide-feather');
|
|
expect(featherIcon).toBeInTheDocument();
|
|
});
|
|
|
|
it('calls onClick when card is clicked', () => {
|
|
render(<AgentCard agent={mockAgent} onClick={mockOnClick} />);
|
|
|
|
const card = screen.getByRole('button');
|
|
fireEvent.click(card);
|
|
|
|
expect(mockOnClick).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('calls onClick when Enter key is pressed', () => {
|
|
render(<AgentCard agent={mockAgent} onClick={mockOnClick} />);
|
|
|
|
const card = screen.getByRole('button');
|
|
fireEvent.keyDown(card, { key: 'Enter' });
|
|
|
|
expect(mockOnClick).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('calls onClick when Space key is pressed', () => {
|
|
render(<AgentCard agent={mockAgent} onClick={mockOnClick} />);
|
|
|
|
const card = screen.getByRole('button');
|
|
fireEvent.keyDown(card, { key: ' ' });
|
|
|
|
expect(mockOnClick).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('does not call onClick for other keys', () => {
|
|
render(<AgentCard agent={mockAgent} onClick={mockOnClick} />);
|
|
|
|
const card = screen.getByRole('button');
|
|
fireEvent.keyDown(card, { key: 'Escape' });
|
|
|
|
expect(mockOnClick).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('applies additional className when provided', () => {
|
|
render(<AgentCard agent={mockAgent} onClick={mockOnClick} className="custom-class" />);
|
|
|
|
const card = screen.getByRole('button');
|
|
expect(card).toHaveClass('custom-class');
|
|
});
|
|
|
|
it('handles missing support contact gracefully', () => {
|
|
const agentWithoutContact = {
|
|
...mockAgent,
|
|
support_contact: undefined,
|
|
authorName: undefined,
|
|
};
|
|
|
|
render(<AgentCard agent={agentWithoutContact} onClick={mockOnClick} />);
|
|
|
|
expect(screen.getByText('Test Agent')).toBeInTheDocument();
|
|
expect(screen.getByText('A test agent for testing purposes')).toBeInTheDocument();
|
|
expect(screen.queryByText(/Created by/)).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('displays authorName when support_contact is missing', () => {
|
|
const agentWithAuthorName = {
|
|
...mockAgent,
|
|
support_contact: undefined,
|
|
authorName: 'John Doe',
|
|
};
|
|
|
|
render(<AgentCard agent={agentWithAuthorName} onClick={mockOnClick} />);
|
|
|
|
expect(screen.getByText('John Doe')).toBeInTheDocument();
|
|
});
|
|
|
|
it('displays support_contact email when name is missing', () => {
|
|
const agentWithEmailOnly = {
|
|
...mockAgent,
|
|
support_contact: { email: 'contact@example.com' },
|
|
authorName: undefined,
|
|
};
|
|
|
|
render(<AgentCard agent={agentWithEmailOnly} onClick={mockOnClick} />);
|
|
|
|
expect(screen.getByText('contact@example.com')).toBeInTheDocument();
|
|
});
|
|
|
|
it('prioritizes support_contact name over authorName', () => {
|
|
const agentWithBoth = {
|
|
...mockAgent,
|
|
support_contact: { name: 'Support Team' },
|
|
authorName: 'John Doe',
|
|
};
|
|
|
|
render(<AgentCard agent={agentWithBoth} onClick={mockOnClick} />);
|
|
|
|
expect(screen.getByText('Support Team')).toBeInTheDocument();
|
|
expect(screen.queryByText('John Doe')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('prioritizes name over email in support_contact', () => {
|
|
const agentWithNameAndEmail = {
|
|
...mockAgent,
|
|
support_contact: {
|
|
name: 'Support Team',
|
|
email: 'support@example.com',
|
|
},
|
|
authorName: undefined,
|
|
};
|
|
|
|
render(<AgentCard agent={agentWithNameAndEmail} onClick={mockOnClick} />);
|
|
|
|
expect(screen.getByText('Support Team')).toBeInTheDocument();
|
|
expect(screen.queryByText('support@example.com')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('has proper accessibility attributes', () => {
|
|
render(<AgentCard agent={mockAgent} onClick={mockOnClick} />);
|
|
|
|
const card = screen.getByRole('button');
|
|
expect(card).toHaveAttribute('tabIndex', '0');
|
|
expect(card).toHaveAttribute(
|
|
'aria-label',
|
|
'Test Agent agent. A test agent for testing purposes',
|
|
);
|
|
});
|
|
|
|
it('displays localized category label', () => {
|
|
const agentWithCategory = {
|
|
...mockAgent,
|
|
category: 'general',
|
|
};
|
|
|
|
render(<AgentCard agent={agentWithCategory} onClick={mockOnClick} />);
|
|
|
|
expect(screen.getByText('General')).toBeInTheDocument();
|
|
});
|
|
|
|
it('displays custom category label', () => {
|
|
const agentWithCustomCategory = {
|
|
...mockAgent,
|
|
category: 'custom',
|
|
};
|
|
|
|
render(<AgentCard agent={agentWithCustomCategory} onClick={mockOnClick} />);
|
|
|
|
expect(screen.getByText('Custom Category')).toBeInTheDocument();
|
|
});
|
|
|
|
it('displays capitalized fallback for unknown category', () => {
|
|
const agentWithUnknownCategory = {
|
|
...mockAgent,
|
|
category: 'unknown',
|
|
};
|
|
|
|
render(<AgentCard agent={agentWithUnknownCategory} onClick={mockOnClick} />);
|
|
|
|
expect(screen.getByText('Unknown')).toBeInTheDocument();
|
|
});
|
|
|
|
it('does not display category tag when category is not provided', () => {
|
|
render(<AgentCard agent={mockAgent} onClick={mockOnClick} />);
|
|
|
|
expect(screen.queryByText('General')).not.toBeInTheDocument();
|
|
expect(screen.queryByText('Unknown')).not.toBeInTheDocument();
|
|
});
|
|
});
|