mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 17:00:15 +01:00
👤 feat: Agent Avatar Removal and Decouple upload/reset from Agent Updates (#10527)
* ✨ 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>
This commit is contained in:
parent
c0cb48256e
commit
8907bd5d7c
17 changed files with 931 additions and 398 deletions
|
|
@ -0,0 +1,141 @@
|
|||
/**
|
||||
* @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);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue