mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-04-05 23:37:19 +02:00
🧹 chore: Clean Up Config Fields (#12537)
* chore: remove unused `interface.endpointsMenu` config field * chore: address review — restore JSDoc UI-only example, add Zod strip test * chore: remove unused `interface.sidePanel` config field * chore: restrict fileStrategy/fileStrategies schema to valid storage backends * fix: use valid FileStorage value in AppService test * chore: address review — version bump, exhaustiveness guard, JSDoc, configSchema test * chore: remove debug logger.log from MessageIcon render path * fix: rewrite MessageIcon render tests to use render counting instead of logger spying * chore: bump librechat-data-provider to 0.8.407 * chore: sync example YAML version to 1.3.7
This commit is contained in:
parent
b4d97bd888
commit
ea28dbfa89
17 changed files with 211 additions and 190 deletions
|
|
@ -1,10 +1,10 @@
|
|||
import { useMemo, useEffect, useRef, memo } from 'react';
|
||||
import { useMemo, memo } from 'react';
|
||||
import { getEndpointField } from 'librechat-data-provider';
|
||||
import type { Assistant, Agent } from 'librechat-data-provider';
|
||||
import type { TMessageIcon } from '~/common';
|
||||
import ConvoIconURL from '~/components/Endpoints/ConvoIconURL';
|
||||
import { useGetEndpointsQuery } from '~/data-provider';
|
||||
import { getIconEndpoint, logger } from '~/utils';
|
||||
import { getIconEndpoint } from '~/utils';
|
||||
import Icon from '~/components/Endpoints/Icon';
|
||||
|
||||
type MessageIconProps = {
|
||||
|
|
@ -19,25 +19,20 @@ type MessageIconProps = {
|
|||
* this component renders display properties only, not identity-derived content.
|
||||
*/
|
||||
export function arePropsEqual(prev: MessageIconProps, next: MessageIconProps): boolean {
|
||||
const checks: [string, unknown, unknown][] = [
|
||||
['iconData.endpoint', prev.iconData?.endpoint, next.iconData?.endpoint],
|
||||
['iconData.model', prev.iconData?.model, next.iconData?.model],
|
||||
['iconData.iconURL', prev.iconData?.iconURL, next.iconData?.iconURL],
|
||||
['iconData.modelLabel', prev.iconData?.modelLabel, next.iconData?.modelLabel],
|
||||
['iconData.isCreatedByUser', prev.iconData?.isCreatedByUser, next.iconData?.isCreatedByUser],
|
||||
['agent.name', prev.agent?.name, next.agent?.name],
|
||||
['agent.avatar.filepath', prev.agent?.avatar?.filepath, next.agent?.avatar?.filepath],
|
||||
['assistant.name', prev.assistant?.name, next.assistant?.name],
|
||||
[
|
||||
'assistant.metadata.avatar',
|
||||
prev.assistant?.metadata?.avatar,
|
||||
next.assistant?.metadata?.avatar,
|
||||
],
|
||||
const checks: [unknown, unknown][] = [
|
||||
[prev.iconData?.endpoint, next.iconData?.endpoint],
|
||||
[prev.iconData?.model, next.iconData?.model],
|
||||
[prev.iconData?.iconURL, next.iconData?.iconURL],
|
||||
[prev.iconData?.modelLabel, next.iconData?.modelLabel],
|
||||
[prev.iconData?.isCreatedByUser, next.iconData?.isCreatedByUser],
|
||||
[prev.agent?.name, next.agent?.name],
|
||||
[prev.agent?.avatar?.filepath, next.agent?.avatar?.filepath],
|
||||
[prev.assistant?.name, next.assistant?.name],
|
||||
[prev.assistant?.metadata?.avatar, next.assistant?.metadata?.avatar],
|
||||
];
|
||||
|
||||
for (const [field, prevVal, nextVal] of checks) {
|
||||
for (const [prevVal, nextVal] of checks) {
|
||||
if (prevVal !== nextVal) {
|
||||
logger.log('icon_memo_diff', `field "${field}" changed:`, prevVal, '→', nextVal);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -45,28 +40,6 @@ export function arePropsEqual(prev: MessageIconProps, next: MessageIconProps): b
|
|||
}
|
||||
|
||||
const MessageIcon = memo(({ iconData, assistant, agent }: MessageIconProps) => {
|
||||
const renderCountRef = useRef(0);
|
||||
renderCountRef.current += 1;
|
||||
|
||||
useEffect(() => {
|
||||
logger.log(
|
||||
'icon_lifecycle',
|
||||
'MOUNT',
|
||||
iconData?.modelLabel,
|
||||
`render #${renderCountRef.current}`,
|
||||
);
|
||||
return () => {
|
||||
logger.log('icon_lifecycle', 'UNMOUNT', iconData?.modelLabel);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
logger.log(
|
||||
'icon_data',
|
||||
`render #${renderCountRef.current}`,
|
||||
iconData?.isCreatedByUser ? 'user' : iconData?.modelLabel,
|
||||
iconData,
|
||||
);
|
||||
const { data: endpointsConfig } = useGetEndpointsQuery();
|
||||
|
||||
const agentName = agent?.name ?? '';
|
||||
|
|
|
|||
|
|
@ -11,29 +11,25 @@ jest.mock('librechat-data-provider', () => ({
|
|||
jest.mock('~/data-provider', () => ({
|
||||
useGetEndpointsQuery: jest.fn(() => ({ data: {} })),
|
||||
}));
|
||||
|
||||
// logger is a plain object with a real function — not a jest.fn() —
|
||||
// so restoreMocks/clearMocks won't touch it. We spy on it per-test instead.
|
||||
const logCalls: unknown[][] = [];
|
||||
jest.mock('~/utils', () => ({
|
||||
getIconEndpoint: jest.fn(() => 'agents'),
|
||||
logger: {
|
||||
log: (...args: unknown[]) => {
|
||||
logCalls.push(args);
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const iconRenderCount = { current: 0 };
|
||||
|
||||
jest.mock('~/components/Endpoints/ConvoIconURL', () => {
|
||||
const ConvoIconURL = (props: Record<string, unknown>) => (
|
||||
<div data-testid="convo-icon-url" data-icon-url={props.iconURL as string} />
|
||||
);
|
||||
const ConvoIconURL = (props: Record<string, unknown>) => {
|
||||
iconRenderCount.current += 1;
|
||||
return <div data-testid="convo-icon-url" data-icon-url={props.iconURL as string} />;
|
||||
};
|
||||
ConvoIconURL.displayName = 'ConvoIconURL';
|
||||
return { __esModule: true, default: ConvoIconURL };
|
||||
});
|
||||
jest.mock('~/components/Endpoints/Icon', () => {
|
||||
const Icon = (props: Record<string, unknown>) => (
|
||||
<div data-testid="icon" data-icon-url={props.iconURL as string} />
|
||||
);
|
||||
const Icon = (props: Record<string, unknown>) => {
|
||||
iconRenderCount.current += 1;
|
||||
return <div data-testid="icon" data-icon-url={props.iconURL as string} />;
|
||||
};
|
||||
Icon.displayName = 'Icon';
|
||||
return { __esModule: true, default: Icon };
|
||||
});
|
||||
|
|
@ -58,79 +54,68 @@ const baseIconData: TMessageIcon = {
|
|||
|
||||
describe('MessageIcon render cycles', () => {
|
||||
beforeEach(() => {
|
||||
logCalls.length = 0;
|
||||
iconRenderCount.current = 0;
|
||||
});
|
||||
|
||||
it('renders once on initial mount', () => {
|
||||
render(<MessageIcon iconData={baseIconData} agent={makeAgent()} />);
|
||||
const iconDataCalls = logCalls.filter((c) => c[0] === 'icon_data');
|
||||
expect(iconDataCalls).toHaveLength(1);
|
||||
expect(iconRenderCount.current).toBe(1);
|
||||
});
|
||||
|
||||
it('does not re-render when parent re-renders with same field values but new object references', () => {
|
||||
const agent = makeAgent();
|
||||
const { rerender } = render(<MessageIcon iconData={baseIconData} agent={agent} />);
|
||||
logCalls.length = 0;
|
||||
iconRenderCount.current = 0;
|
||||
|
||||
// Simulate parent re-render: new iconData object (same field values), new agent object (same data)
|
||||
rerender(<MessageIcon iconData={{ ...baseIconData }} agent={makeAgent()} />);
|
||||
|
||||
const iconDataCalls = logCalls.filter((c) => c[0] === 'icon_data');
|
||||
expect(iconDataCalls).toHaveLength(0);
|
||||
expect(iconRenderCount.current).toBe(0);
|
||||
});
|
||||
|
||||
it('does not re-render when agent object reference changes but name and avatar are the same', () => {
|
||||
const agent1 = makeAgent();
|
||||
const { rerender } = render(<MessageIcon iconData={baseIconData} agent={agent1} />);
|
||||
logCalls.length = 0;
|
||||
iconRenderCount.current = 0;
|
||||
|
||||
// New agent object with different id but same display fields
|
||||
const agent2 = makeAgent({ id: 'agent_456' });
|
||||
rerender(<MessageIcon iconData={baseIconData} agent={agent2} />);
|
||||
|
||||
const iconDataCalls = logCalls.filter((c) => c[0] === 'icon_data');
|
||||
expect(iconDataCalls).toHaveLength(0);
|
||||
expect(iconRenderCount.current).toBe(0);
|
||||
});
|
||||
|
||||
it('re-renders when agent avatar filepath changes', () => {
|
||||
const agent1 = makeAgent();
|
||||
const { rerender } = render(<MessageIcon iconData={baseIconData} agent={agent1} />);
|
||||
logCalls.length = 0;
|
||||
iconRenderCount.current = 0;
|
||||
|
||||
const agent2 = makeAgent({ avatar: { filepath: '/images/new-avatar.png' } });
|
||||
rerender(<MessageIcon iconData={baseIconData} agent={agent2} />);
|
||||
|
||||
const iconDataCalls = logCalls.filter((c) => c[0] === 'icon_data');
|
||||
expect(iconDataCalls).toHaveLength(1);
|
||||
expect(iconRenderCount.current).toBe(1);
|
||||
});
|
||||
|
||||
it('re-renders when agent goes from undefined to defined (name changes from undefined to string)', () => {
|
||||
const { rerender } = render(<MessageIcon iconData={baseIconData} agent={undefined} />);
|
||||
logCalls.length = 0;
|
||||
iconRenderCount.current = 0;
|
||||
|
||||
rerender(<MessageIcon iconData={baseIconData} agent={makeAgent()} />);
|
||||
|
||||
const iconDataCalls = logCalls.filter((c) => c[0] === 'icon_data');
|
||||
expect(iconDataCalls).toHaveLength(1);
|
||||
expect(iconRenderCount.current).toBe(1);
|
||||
});
|
||||
|
||||
describe('simulates message lifecycle', () => {
|
||||
it('renders exactly twice during new message + streaming start: initial render + modelLabel update', () => {
|
||||
// Phase 1: Initial response message created by useChatFunctions
|
||||
// model is set to agent_id, iconURL is undefined, modelLabel is '' or sender
|
||||
const initialIconData: TMessageIcon = {
|
||||
endpoint: EModelEndpoint.agents,
|
||||
model: 'agent_123',
|
||||
iconURL: undefined,
|
||||
modelLabel: '', // Not yet resolved
|
||||
modelLabel: '',
|
||||
isCreatedByUser: false,
|
||||
};
|
||||
const agent = makeAgent();
|
||||
|
||||
const { rerender } = render(<MessageIcon iconData={initialIconData} agent={agent} />);
|
||||
|
||||
// Phase 2: First streaming chunk arrives, messageLabel resolves to agent name
|
||||
// This is a legitimate re-render — modelLabel changed from '' to 'GitHub Agent'
|
||||
const streamingIconData: TMessageIcon = {
|
||||
...initialIconData,
|
||||
modelLabel: 'GitHub Agent',
|
||||
|
|
@ -138,9 +123,7 @@ describe('MessageIcon render cycles', () => {
|
|||
|
||||
rerender(<MessageIcon iconData={streamingIconData} agent={agent} />);
|
||||
|
||||
const iconDataCalls = logCalls.filter((c) => c[0] === 'icon_data');
|
||||
// Exactly 2: initial mount + modelLabel change
|
||||
expect(iconDataCalls).toHaveLength(2);
|
||||
expect(iconRenderCount.current).toBe(2);
|
||||
});
|
||||
|
||||
it('does NOT re-render on subsequent streaming chunks (content changes, isSubmitting stays true)', () => {
|
||||
|
|
@ -154,17 +137,13 @@ describe('MessageIcon render cycles', () => {
|
|||
const agent = makeAgent();
|
||||
|
||||
const { rerender } = render(<MessageIcon iconData={iconData} agent={agent} />);
|
||||
logCalls.length = 0;
|
||||
iconRenderCount.current = 0;
|
||||
|
||||
// Simulate multiple parent re-renders from streaming chunks
|
||||
// Parent (ContentRender) re-renders because chatContext changed,
|
||||
// but MessageIcon props are identical field-by-field
|
||||
for (let i = 0; i < 5; i++) {
|
||||
rerender(<MessageIcon iconData={{ ...iconData }} agent={makeAgent()} />);
|
||||
}
|
||||
|
||||
const iconDataCalls = logCalls.filter((c) => c[0] === 'icon_data');
|
||||
expect(iconDataCalls).toHaveLength(0);
|
||||
expect(iconRenderCount.current).toBe(0);
|
||||
});
|
||||
|
||||
it('does NOT re-render when agentsMap context updates with same agent data', () => {
|
||||
|
|
@ -176,18 +155,15 @@ describe('MessageIcon render cycles', () => {
|
|||
isCreatedByUser: false,
|
||||
};
|
||||
|
||||
// First render with agent from original agentsMap
|
||||
const agent1 = makeAgent();
|
||||
const { rerender } = render(<MessageIcon iconData={iconData} agent={agent1} />);
|
||||
logCalls.length = 0;
|
||||
iconRenderCount.current = 0;
|
||||
|
||||
// agentsMap refetched → new agent object, same display data
|
||||
const agent2 = makeAgent();
|
||||
expect(agent1).not.toBe(agent2); // different reference
|
||||
expect(agent1).not.toBe(agent2);
|
||||
rerender(<MessageIcon iconData={iconData} agent={agent2} />);
|
||||
|
||||
const iconDataCalls = logCalls.filter((c) => c[0] === 'icon_data');
|
||||
expect(iconDataCalls).toHaveLength(0);
|
||||
expect(iconRenderCount.current).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue