mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 00:40:14 +01:00
* ✨ feat: Enhance agent avatar management with upload and reset functionality * ✨ feat: Refactor AvatarMenu to use DropdownPopup for improved UI and functionality * ✨ feat: Improve avatar upload handling in AgentPanel to suppress misleading "no changes" toast * ✨ feat: Refactor toast message handling and payload composition in AgentPanel for improved clarity and functionality * ✨ feat: Enhance agent avatar functionality with upload, reset, and validation improvements * ✨ feat: Refactor agent avatar upload handling and enhance related components for improved functionality and user experience * feat(agents): tighten ACL, harden GETs/search, and sanitize action metadata stop persisting refreshed S3 URLs on GET; compute per-response only enforce ACL EDIT on revert route; remove legacy admin/author/collab checks sanitize action metadata before persisting during duplication (api_key, oauth_client_id, oauth_client_secret) escape user search input, cap length (100), and use Set for public flag mapping add explicit req.file guard in avatar upload; fix empty catch lint; remove unused imports * feat: Remove outdated avatar-related translation keys * feat: Improve error logging for avatar updates and streamline file input handling * feat(agents): implement caching for S3 avatar refresh in agent list responses * fix: replace unconventional 'void e' with explicit comment to clarify intentionally ignored error Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * feat(agents): enhance avatar handling and improve search functionality * fix: clarify intentionally ignored error in agent list handler --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
141 lines
3.8 KiB
TypeScript
141 lines
3.8 KiB
TypeScript
/**
|
|
* @jest-environment jsdom
|
|
*/
|
|
import { describe, it, expect, jest } from '@jest/globals';
|
|
import { Constants, type Agent } from 'librechat-data-provider';
|
|
import type { FieldNamesMarkedBoolean } from 'react-hook-form';
|
|
import type { AgentForm } from '~/common';
|
|
import {
|
|
composeAgentUpdatePayload,
|
|
persistAvatarChanges,
|
|
isAvatarUploadOnlyDirty,
|
|
} from '../AgentPanel';
|
|
|
|
const createForm = (): AgentForm => ({
|
|
agent: undefined,
|
|
id: 'agent_123',
|
|
name: 'Agent',
|
|
description: null,
|
|
instructions: null,
|
|
model: 'gpt-4',
|
|
model_parameters: {},
|
|
tools: [],
|
|
provider: 'openai',
|
|
agent_ids: [],
|
|
edges: [],
|
|
end_after_tools: false,
|
|
hide_sequential_outputs: false,
|
|
recursion_limit: undefined,
|
|
category: 'general',
|
|
support_contact: undefined,
|
|
artifacts: '',
|
|
execute_code: false,
|
|
file_search: false,
|
|
web_search: false,
|
|
avatar_file: null,
|
|
avatar_preview: '',
|
|
avatar_action: null,
|
|
});
|
|
|
|
describe('composeAgentUpdatePayload', () => {
|
|
it('includes avatar: null when resetting a persistent agent', () => {
|
|
const form = createForm();
|
|
form.avatar_action = 'reset';
|
|
|
|
const { payload } = composeAgentUpdatePayload(form, 'agent_123');
|
|
|
|
expect(payload.avatar).toBeNull();
|
|
});
|
|
|
|
it('omits avatar when resetting an ephemeral agent', () => {
|
|
const form = createForm();
|
|
form.avatar_action = 'reset';
|
|
|
|
const { payload } = composeAgentUpdatePayload(form, Constants.EPHEMERAL_AGENT_ID);
|
|
|
|
expect(payload.avatar).toBeUndefined();
|
|
});
|
|
|
|
it('never adds avatar during upload actions', () => {
|
|
const form = createForm();
|
|
form.avatar_action = 'upload';
|
|
|
|
const { payload } = composeAgentUpdatePayload(form, 'agent_123');
|
|
|
|
expect(payload.avatar).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('persistAvatarChanges', () => {
|
|
it('returns false for ephemeral agents', async () => {
|
|
const uploadAvatar = jest.fn();
|
|
const result = await persistAvatarChanges({
|
|
agentId: Constants.EPHEMERAL_AGENT_ID,
|
|
avatarActionState: 'upload',
|
|
avatarFile: new File(['avatar'], 'avatar.png', { type: 'image/png' }),
|
|
uploadAvatar,
|
|
});
|
|
|
|
expect(result).toBe(false);
|
|
expect(uploadAvatar).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('returns false when no upload is pending', async () => {
|
|
const uploadAvatar = jest.fn();
|
|
const result = await persistAvatarChanges({
|
|
agentId: 'agent_123',
|
|
avatarActionState: null,
|
|
avatarFile: null,
|
|
uploadAvatar,
|
|
});
|
|
|
|
expect(result).toBe(false);
|
|
expect(uploadAvatar).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('uploads avatar when all prerequisites are met', async () => {
|
|
const uploadAvatar = jest.fn().mockResolvedValue({} as Agent);
|
|
const file = new File(['avatar'], 'avatar.png', { type: 'image/png' });
|
|
|
|
const result = await persistAvatarChanges({
|
|
agentId: 'agent_123',
|
|
avatarActionState: 'upload',
|
|
avatarFile: file,
|
|
uploadAvatar,
|
|
});
|
|
|
|
expect(result).toBe(true);
|
|
expect(uploadAvatar).toHaveBeenCalledTimes(1);
|
|
const callArgs = uploadAvatar.mock.calls[0][0];
|
|
expect(callArgs.agent_id).toBe('agent_123');
|
|
expect(callArgs.formData).toBeInstanceOf(FormData);
|
|
});
|
|
});
|
|
|
|
describe('isAvatarUploadOnlyDirty', () => {
|
|
it('detects avatar-only dirty state', () => {
|
|
const dirtyFields = {
|
|
avatar_action: true,
|
|
avatar_preview: true,
|
|
} as FieldNamesMarkedBoolean<AgentForm>;
|
|
|
|
expect(isAvatarUploadOnlyDirty(dirtyFields)).toBe(true);
|
|
});
|
|
|
|
it('ignores agent field when checking dirty state', () => {
|
|
const dirtyFields = {
|
|
agent: { value: true } as any,
|
|
avatar_file: true,
|
|
} as FieldNamesMarkedBoolean<AgentForm>;
|
|
|
|
expect(isAvatarUploadOnlyDirty(dirtyFields)).toBe(true);
|
|
});
|
|
|
|
it('returns false when other fields are dirty', () => {
|
|
const dirtyFields = {
|
|
name: true,
|
|
} as FieldNamesMarkedBoolean<AgentForm>;
|
|
|
|
expect(isAvatarUploadOnlyDirty(dirtyFields)).toBe(false);
|
|
});
|
|
});
|