LibreChat/client/src/components/SidePanel/Agents/__tests__/AgentPanel.helpers.spec.ts
Marco Beretta 8907bd5d7c
👤 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>
2025-11-17 17:04:01 -05:00

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);
});
});