mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-03-16 04:36:34 +01:00
⛵ fix: Resolve Agent Provider Endpoint Type for File Upload Support (#12117)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
* chore: Remove unused setValueOnChange prop from MCPServerMenuItem component
* fix: Resolve agent provider endpoint type for file upload support
When using the agents endpoint with a custom provider (e.g., Moonshot),
the endpointType was resolving to "agents" instead of the provider's
actual type ("custom"), causing "Upload to Provider" to not appear in
the file attach menu.
Adds `resolveEndpointType` utility in data-provider that follows the
chain: endpoint (if not agents) → agent.provider → agents. Applied
consistently across AttachFileChat, DragDropContext, useDragHelpers,
and AgentPanel file components (FileContext, FileSearch, Code/Files).
* refactor: Extract useAgentFileConfig hook, restore deleted tests, fix review findings
- Extract shared provider resolution logic into useAgentFileConfig hook
(Finding #2: DRY violation across FileContext, FileSearch, Code/Files)
- Restore 18 deleted test cases in AttachFileMenu.spec.tsx covering
agent capabilities, SharePoint, edge cases, and button state
(Finding #1: accidental test deletion)
- Wrap fileConfigEndpoint in useMemo in AttachFileChat (Finding #3)
- Fix misleading test name in AgentFileConfig.spec.tsx (Finding #4)
- Fix import order in FileSearch.tsx, FileContext.tsx, Code/Files.tsx (Finding #5)
- Add comment about cache gap in useDragHelpers (Finding #6)
- Clarify resolveEndpointType JSDoc (Finding #7)
* refactor: Memoize Footer component for performance optimization
- Converted Footer component to a memoized version to prevent unnecessary re-renders.
- Improved import structure by adding memo to the React import statement for clarity.
* chore: Fix remaining review nits
- Widen useAgentFileConfig return type to EModelEndpoint | string
- Fix import order in FileContext.tsx and FileSearch.tsx
- Remove dead endpointType param from setupMocks in AttachFileMenu test
* fix: Pass resolved provider endpoint to file upload validation
AgentPanel file components (FileContext, FileSearch, Code/Files) were
hardcoding endpointOverride to "agents", causing both client-side
validation (file limits, MIME types) and server-side validation to
use the agents config instead of the provider-specific config.
Adds endpointTypeOverride to UseFileHandling params so endpoint and
endpointType can be set independently. Components now pass the
resolved provider name and type from useAgentFileConfig, so the full
fallback chain (provider → custom → agents → default) applies to
file upload validation on both client and server.
* test: Verify any custom endpoint is document-supported regardless of name
Adds parameterized tests with arbitrary endpoint names (spaces, hyphens,
colons, etc.) confirming that all custom endpoints resolve to
document-supported through resolveEndpointType, both as direct
endpoints and as agent providers.
* fix: Use || for provider fallback, test endpointOverride wiring
- Change providerValue ?? to providerValue || so empty string is
treated as "no provider" consistently with resolveEndpointType
- Add wiring tests to CodeFiles, FileContext, FileSearch verifying
endpointOverride and endpointTypeOverride are passed correctly
- Update endpointOverride JSDoc to document endpointType fallback
This commit is contained in:
parent
cfaa6337c1
commit
2ac62a2e71
22 changed files with 1573 additions and 582 deletions
|
|
@ -1,18 +1,11 @@
|
|||
import { memo, useMemo, useRef, useState } from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { AttachmentIcon } from '@librechat/client';
|
||||
import {
|
||||
EToolResources,
|
||||
EModelEndpoint,
|
||||
mergeFileConfig,
|
||||
AgentCapabilities,
|
||||
getEndpointFileConfig,
|
||||
} from 'librechat-data-provider';
|
||||
import { EToolResources, EModelEndpoint, AgentCapabilities } from 'librechat-data-provider';
|
||||
import type { ExtendedFile, AgentForm } from '~/common';
|
||||
import { useFileHandlingNoChatContext } from '~/hooks/Files/useFileHandling';
|
||||
import { useAgentFileConfig, useLocalize, useLazyEffect } from '~/hooks';
|
||||
import FileRow from '~/components/Chat/Input/Files/FileRow';
|
||||
import { useLocalize, useLazyEffect } from '~/hooks';
|
||||
import { useGetFileConfig } from '~/data-provider';
|
||||
import { isEphemeralAgent } from '~/common';
|
||||
|
||||
const tool_resource = EToolResources.execute_code;
|
||||
|
|
@ -29,14 +22,14 @@ function Files({
|
|||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [files, setFiles] = useState<Map<string, ExtendedFile>>(new Map());
|
||||
const fileHandlingState = useMemo(() => ({ files, setFiles, conversation: null }), [files]);
|
||||
const { data: fileConfig = null } = useGetFileConfig({
|
||||
select: (data) => mergeFileConfig(data),
|
||||
});
|
||||
const { endpointFileConfig, providerValue, endpointType } = useAgentFileConfig();
|
||||
const endpointOverride = providerValue || EModelEndpoint.agents;
|
||||
const { abortUpload, handleFileChange } = useFileHandlingNoChatContext(
|
||||
{
|
||||
fileSetter: setFiles,
|
||||
additionalMetadata: { agent_id, tool_resource },
|
||||
endpointOverride: EModelEndpoint.agents,
|
||||
endpointOverride,
|
||||
endpointTypeOverride: endpointType,
|
||||
},
|
||||
fileHandlingState,
|
||||
);
|
||||
|
|
@ -52,12 +45,6 @@ function Files({
|
|||
);
|
||||
|
||||
const codeChecked = watch(AgentCapabilities.execute_code);
|
||||
|
||||
const endpointFileConfig = getEndpointFileConfig({
|
||||
fileConfig,
|
||||
endpoint: EModelEndpoint.agents,
|
||||
endpointType: EModelEndpoint.agents,
|
||||
});
|
||||
const isUploadDisabled = endpointFileConfig?.disabled ?? false;
|
||||
|
||||
if (isUploadDisabled) {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,7 @@
|
|||
import { memo, useMemo, useRef, useState } from 'react';
|
||||
import { Folder } from 'lucide-react';
|
||||
import * as Ariakit from '@ariakit/react';
|
||||
import {
|
||||
EModelEndpoint,
|
||||
EToolResources,
|
||||
mergeFileConfig,
|
||||
getEndpointFileConfig,
|
||||
} from 'librechat-data-provider';
|
||||
import { EModelEndpoint, EToolResources } from 'librechat-data-provider';
|
||||
import {
|
||||
HoverCard,
|
||||
DropdownPopup,
|
||||
|
|
@ -18,13 +13,13 @@ import {
|
|||
HoverCardTrigger,
|
||||
} from '@librechat/client';
|
||||
import type { ExtendedFile } from '~/common';
|
||||
import { useLocalize, useLazyEffect } from '~/hooks';
|
||||
import { useGetFileConfig, useGetStartupConfig } from '~/data-provider';
|
||||
import { SharePointPickerDialog } from '~/components/SharePoint';
|
||||
import FileRow from '~/components/Chat/Input/Files/FileRow';
|
||||
import { ESide, isEphemeralAgent } from '~/common';
|
||||
import { useSharePointFileHandlingNoChatContext } from '~/hooks/Files/useSharePointFileHandling';
|
||||
import { useFileHandlingNoChatContext } from '~/hooks/Files/useFileHandling';
|
||||
import { useAgentFileConfig, useLocalize, useLazyEffect } from '~/hooks';
|
||||
import { SharePointPickerDialog } from '~/components/SharePoint';
|
||||
import FileRow from '~/components/Chat/Input/Files/FileRow';
|
||||
import { useGetStartupConfig } from '~/data-provider';
|
||||
import { ESide, isEphemeralAgent } from '~/common';
|
||||
|
||||
function FileContext({
|
||||
agent_id,
|
||||
|
|
@ -41,15 +36,14 @@ function FileContext({
|
|||
const [isSharePointDialogOpen, setIsSharePointDialogOpen] = useState(false);
|
||||
const { data: startupConfig } = useGetStartupConfig();
|
||||
const sharePointEnabled = startupConfig?.sharePointFilePickerEnabled;
|
||||
|
||||
const { data: fileConfig = null } = useGetFileConfig({
|
||||
select: (data) => mergeFileConfig(data),
|
||||
});
|
||||
const { endpointFileConfig, providerValue, endpointType } = useAgentFileConfig();
|
||||
const endpointOverride = providerValue || EModelEndpoint.agents;
|
||||
|
||||
const { handleFileChange } = useFileHandlingNoChatContext(
|
||||
{
|
||||
additionalMetadata: { agent_id, tool_resource: EToolResources.context },
|
||||
endpointOverride: EModelEndpoint.agents,
|
||||
endpointOverride,
|
||||
endpointTypeOverride: endpointType,
|
||||
fileSetter: setFiles,
|
||||
},
|
||||
fileHandlingState,
|
||||
|
|
@ -58,7 +52,8 @@ function FileContext({
|
|||
useSharePointFileHandlingNoChatContext(
|
||||
{
|
||||
additionalMetadata: { agent_id, tool_resource: EToolResources.file_search },
|
||||
endpointOverride: EModelEndpoint.agents,
|
||||
endpointOverride,
|
||||
endpointTypeOverride: endpointType,
|
||||
fileSetter: setFiles,
|
||||
},
|
||||
fileHandlingState,
|
||||
|
|
@ -72,12 +67,6 @@ function FileContext({
|
|||
[_files],
|
||||
750,
|
||||
);
|
||||
|
||||
const endpointFileConfig = getEndpointFileConfig({
|
||||
fileConfig,
|
||||
endpoint: EModelEndpoint.agents,
|
||||
endpointType: EModelEndpoint.agents,
|
||||
});
|
||||
const isUploadDisabled = endpointFileConfig?.disabled ?? false;
|
||||
const handleSharePointFilesSelected = async (sharePointFiles: any[]) => {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -3,22 +3,16 @@ import { Folder } from 'lucide-react';
|
|||
import * as Ariakit from '@ariakit/react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { SharePointIcon, AttachmentIcon, DropdownPopup } from '@librechat/client';
|
||||
import {
|
||||
EModelEndpoint,
|
||||
EToolResources,
|
||||
mergeFileConfig,
|
||||
AgentCapabilities,
|
||||
getEndpointFileConfig,
|
||||
} from 'librechat-data-provider';
|
||||
import { EModelEndpoint, EToolResources, AgentCapabilities } from 'librechat-data-provider';
|
||||
import type { ExtendedFile, AgentForm } from '~/common';
|
||||
import { useGetFileConfig, useGetStartupConfig } from '~/data-provider';
|
||||
import { useLocalize, useLazyEffect } from '~/hooks';
|
||||
import { useSharePointFileHandlingNoChatContext } from '~/hooks/Files/useSharePointFileHandling';
|
||||
import { useFileHandlingNoChatContext } from '~/hooks/Files/useFileHandling';
|
||||
import { useAgentFileConfig, useLocalize, useLazyEffect } from '~/hooks';
|
||||
import { SharePointPickerDialog } from '~/components/SharePoint';
|
||||
import FileRow from '~/components/Chat/Input/Files/FileRow';
|
||||
import { useGetStartupConfig } from '~/data-provider';
|
||||
import FileSearchCheckbox from './FileSearchCheckbox';
|
||||
import { isEphemeralAgent } from '~/common';
|
||||
import { useFileHandlingNoChatContext } from '~/hooks/Files/useFileHandling';
|
||||
import { useSharePointFileHandlingNoChatContext } from '~/hooks/Files/useSharePointFileHandling';
|
||||
|
||||
function FileSearch({
|
||||
agent_id,
|
||||
|
|
@ -37,15 +31,14 @@ function FileSearch({
|
|||
|
||||
// Get startup configuration for SharePoint feature flag
|
||||
const { data: startupConfig } = useGetStartupConfig();
|
||||
|
||||
const { data: fileConfig = null } = useGetFileConfig({
|
||||
select: (data) => mergeFileConfig(data),
|
||||
});
|
||||
const { endpointFileConfig, providerValue, endpointType } = useAgentFileConfig();
|
||||
const endpointOverride = providerValue || EModelEndpoint.agents;
|
||||
|
||||
const { handleFileChange } = useFileHandlingNoChatContext(
|
||||
{
|
||||
additionalMetadata: { agent_id, tool_resource: EToolResources.file_search },
|
||||
endpointOverride: EModelEndpoint.agents,
|
||||
endpointOverride,
|
||||
endpointTypeOverride: endpointType,
|
||||
fileSetter: setFiles,
|
||||
},
|
||||
fileHandlingState,
|
||||
|
|
@ -55,7 +48,8 @@ function FileSearch({
|
|||
useSharePointFileHandlingNoChatContext(
|
||||
{
|
||||
additionalMetadata: { agent_id, tool_resource: EToolResources.file_search },
|
||||
endpointOverride: EModelEndpoint.agents,
|
||||
endpointOverride,
|
||||
endpointTypeOverride: endpointType,
|
||||
fileSetter: setFiles,
|
||||
},
|
||||
fileHandlingState,
|
||||
|
|
@ -72,12 +66,6 @@ function FileSearch({
|
|||
);
|
||||
|
||||
const fileSearchChecked = watch(AgentCapabilities.file_search);
|
||||
|
||||
const endpointFileConfig = getEndpointFileConfig({
|
||||
fileConfig,
|
||||
endpoint: EModelEndpoint.agents,
|
||||
endpointType: EModelEndpoint.agents,
|
||||
});
|
||||
const isUploadDisabled = endpointFileConfig?.disabled ?? false;
|
||||
|
||||
const sharePointEnabled = startupConfig?.sharePointFilePickerEnabled;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,151 @@
|
|||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { useForm, FormProvider } from 'react-hook-form';
|
||||
import { EModelEndpoint, mergeFileConfig, resolveEndpointType } from 'librechat-data-provider';
|
||||
import type { TEndpointsConfig } from 'librechat-data-provider';
|
||||
import type { AgentForm } from '~/common';
|
||||
import useAgentFileConfig from '~/hooks/Agents/useAgentFileConfig';
|
||||
|
||||
/**
|
||||
* Tests the useAgentFileConfig hook used by FileContext, FileSearch, and Code/Files.
|
||||
* Uses the real hook with mocked data-fetching layer.
|
||||
*/
|
||||
|
||||
const mockEndpointsConfig: TEndpointsConfig = {
|
||||
[EModelEndpoint.openAI]: { userProvide: false, order: 0 },
|
||||
[EModelEndpoint.agents]: { userProvide: false, order: 1 },
|
||||
Moonshot: { type: EModelEndpoint.custom, userProvide: false, order: 9999 },
|
||||
'Some Endpoint': { type: EModelEndpoint.custom, userProvide: false, order: 9999 },
|
||||
};
|
||||
|
||||
let mockFileConfig = mergeFileConfig({
|
||||
endpoints: {
|
||||
Moonshot: { fileLimit: 5 },
|
||||
[EModelEndpoint.agents]: { fileLimit: 20 },
|
||||
default: { fileLimit: 10 },
|
||||
},
|
||||
});
|
||||
|
||||
jest.mock('~/data-provider', () => ({
|
||||
useGetEndpointsQuery: () => ({ data: mockEndpointsConfig }),
|
||||
useGetFileConfig: ({ select }: { select?: (data: unknown) => unknown }) => ({
|
||||
data: select != null ? select(mockFileConfig) : mockFileConfig,
|
||||
}),
|
||||
}));
|
||||
|
||||
function FileConfigProbe() {
|
||||
const { endpointType, endpointFileConfig } = useAgentFileConfig();
|
||||
return (
|
||||
<div>
|
||||
<span data-testid="endpointType">{String(endpointType)}</span>
|
||||
<span data-testid="fileLimit">{endpointFileConfig.fileLimit}</span>
|
||||
<span data-testid="disabled">{String(endpointFileConfig.disabled ?? false)}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TestWrapper({ provider }: { provider?: string | { label: string; value: string } }) {
|
||||
const methods = useForm<AgentForm>({
|
||||
defaultValues: { provider: provider as AgentForm['provider'] },
|
||||
});
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<FileConfigProbe />
|
||||
</FormProvider>
|
||||
);
|
||||
}
|
||||
|
||||
describe('AgentPanel file config resolution (useAgentFileConfig)', () => {
|
||||
describe('endpointType resolution from form provider', () => {
|
||||
it('resolves to custom when provider is a custom endpoint string', () => {
|
||||
render(<TestWrapper provider="Moonshot" />);
|
||||
expect(screen.getByTestId('endpointType').textContent).toBe(EModelEndpoint.custom);
|
||||
});
|
||||
|
||||
it('resolves to custom when provider is a custom endpoint with spaces', () => {
|
||||
render(<TestWrapper provider="Some Endpoint" />);
|
||||
expect(screen.getByTestId('endpointType').textContent).toBe(EModelEndpoint.custom);
|
||||
});
|
||||
|
||||
it('resolves to openAI when provider is openAI', () => {
|
||||
render(<TestWrapper provider={EModelEndpoint.openAI} />);
|
||||
expect(screen.getByTestId('endpointType').textContent).toBe(EModelEndpoint.openAI);
|
||||
});
|
||||
|
||||
it('falls back to agents when provider is undefined', () => {
|
||||
render(<TestWrapper />);
|
||||
expect(screen.getByTestId('endpointType').textContent).toBe(EModelEndpoint.agents);
|
||||
});
|
||||
|
||||
it('falls back to agents when provider is empty string', () => {
|
||||
render(<TestWrapper provider="" />);
|
||||
expect(screen.getByTestId('endpointType').textContent).toBe(EModelEndpoint.agents);
|
||||
expect(screen.getByTestId('fileLimit').textContent).toBe('20');
|
||||
});
|
||||
|
||||
it('falls back to agents when provider option has empty value', () => {
|
||||
render(<TestWrapper provider={{ label: '', value: '' }} />);
|
||||
expect(screen.getByTestId('endpointType').textContent).toBe(EModelEndpoint.agents);
|
||||
expect(screen.getByTestId('fileLimit').textContent).toBe('20');
|
||||
});
|
||||
|
||||
it('resolves correctly when provider is an option object', () => {
|
||||
render(<TestWrapper provider={{ label: 'Moonshot', value: 'Moonshot' }} />);
|
||||
expect(screen.getByTestId('endpointType').textContent).toBe(EModelEndpoint.custom);
|
||||
});
|
||||
});
|
||||
|
||||
describe('file config fallback chain', () => {
|
||||
it('uses Moonshot-specific file config when provider is Moonshot', () => {
|
||||
render(<TestWrapper provider="Moonshot" />);
|
||||
expect(screen.getByTestId('fileLimit').textContent).toBe('5');
|
||||
});
|
||||
|
||||
it('falls back to agents file config when provider has no specific config', () => {
|
||||
render(<TestWrapper provider="Some Endpoint" />);
|
||||
expect(screen.getByTestId('fileLimit').textContent).toBe('20');
|
||||
});
|
||||
|
||||
it('uses agents file config when no provider is set', () => {
|
||||
render(<TestWrapper />);
|
||||
expect(screen.getByTestId('fileLimit').textContent).toBe('20');
|
||||
});
|
||||
|
||||
it('falls back to default config for openAI provider (no openAI-specific config)', () => {
|
||||
render(<TestWrapper provider={EModelEndpoint.openAI} />);
|
||||
expect(screen.getByTestId('fileLimit').textContent).toBe('10');
|
||||
});
|
||||
});
|
||||
|
||||
describe('disabled state', () => {
|
||||
it('reports not disabled for standard config', () => {
|
||||
render(<TestWrapper provider="Moonshot" />);
|
||||
expect(screen.getByTestId('disabled').textContent).toBe('false');
|
||||
});
|
||||
|
||||
it('reports disabled when provider-specific config is disabled', () => {
|
||||
const original = mockFileConfig;
|
||||
mockFileConfig = mergeFileConfig({
|
||||
endpoints: {
|
||||
Moonshot: { disabled: true },
|
||||
[EModelEndpoint.agents]: { fileLimit: 20 },
|
||||
default: { fileLimit: 10 },
|
||||
},
|
||||
});
|
||||
|
||||
render(<TestWrapper provider="Moonshot" />);
|
||||
expect(screen.getByTestId('disabled').textContent).toBe('true');
|
||||
|
||||
mockFileConfig = original;
|
||||
});
|
||||
});
|
||||
|
||||
describe('consistency with direct custom endpoint', () => {
|
||||
it('resolves to the same type as a direct custom endpoint would', () => {
|
||||
render(<TestWrapper provider="Moonshot" />);
|
||||
const agentEndpointType = screen.getByTestId('endpointType').textContent;
|
||||
const directEndpointType = resolveEndpointType(mockEndpointsConfig, 'Moonshot');
|
||||
expect(agentEndpointType).toBe(directEndpointType);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { useForm, FormProvider } from 'react-hook-form';
|
||||
import { EModelEndpoint, mergeFileConfig } from 'librechat-data-provider';
|
||||
import type { TEndpointsConfig } from 'librechat-data-provider';
|
||||
import type { AgentForm } from '~/common';
|
||||
import Files from '../Code/Files';
|
||||
|
||||
const mockEndpointsConfig: TEndpointsConfig = {
|
||||
[EModelEndpoint.agents]: { userProvide: false, order: 1 },
|
||||
Moonshot: { type: EModelEndpoint.custom, userProvide: false, order: 9999 },
|
||||
};
|
||||
|
||||
let mockFileConfig = mergeFileConfig({ endpoints: { default: { fileLimit: 10 } } });
|
||||
|
||||
jest.mock('~/data-provider', () => ({
|
||||
useGetEndpointsQuery: () => ({ data: mockEndpointsConfig }),
|
||||
useGetFileConfig: ({ select }: { select?: (d: unknown) => unknown }) => ({
|
||||
data: select != null ? select(mockFileConfig) : mockFileConfig,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('~/hooks', () => ({
|
||||
useAgentFileConfig: jest.requireActual('~/hooks/Agents/useAgentFileConfig').default,
|
||||
useLocalize: () => (key: string) => key,
|
||||
useLazyEffect: () => {},
|
||||
}));
|
||||
|
||||
const mockUseFileHandlingNoChatContext = jest.fn().mockReturnValue({
|
||||
abortUpload: jest.fn(),
|
||||
handleFileChange: jest.fn(),
|
||||
});
|
||||
|
||||
jest.mock('~/hooks/Files/useFileHandling', () => ({
|
||||
useFileHandlingNoChatContext: (...args: unknown[]) => mockUseFileHandlingNoChatContext(...args),
|
||||
}));
|
||||
|
||||
jest.mock('~/components/Chat/Input/Files/FileRow', () => () => null);
|
||||
|
||||
jest.mock('@librechat/client', () => ({
|
||||
AttachmentIcon: () => <span />,
|
||||
}));
|
||||
|
||||
function Wrapper({ provider, children }: { provider?: string; children: React.ReactNode }) {
|
||||
const methods = useForm<AgentForm>({
|
||||
defaultValues: { provider: provider as AgentForm['provider'] },
|
||||
});
|
||||
return <FormProvider {...methods}>{children}</FormProvider>;
|
||||
}
|
||||
|
||||
describe('Code/Files', () => {
|
||||
it('renders upload UI when file uploads are not disabled', () => {
|
||||
mockFileConfig = mergeFileConfig({ endpoints: { default: { fileLimit: 10 } } });
|
||||
render(
|
||||
<Wrapper provider="Moonshot">
|
||||
<Files agent_id="agent-1" />
|
||||
</Wrapper>,
|
||||
);
|
||||
expect(screen.getByText('com_assistants_code_interpreter_files')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('returns null when file config is disabled for provider', () => {
|
||||
mockFileConfig = mergeFileConfig({
|
||||
endpoints: { Moonshot: { disabled: true }, default: { fileLimit: 10 } },
|
||||
});
|
||||
const { container } = render(
|
||||
<Wrapper provider="Moonshot">
|
||||
<Files agent_id="agent-1" />
|
||||
</Wrapper>,
|
||||
);
|
||||
expect(container.innerHTML).toBe('');
|
||||
});
|
||||
|
||||
it('returns null when agents endpoint config is disabled and no provider config', () => {
|
||||
mockFileConfig = mergeFileConfig({
|
||||
endpoints: { [EModelEndpoint.agents]: { disabled: true }, default: { fileLimit: 10 } },
|
||||
});
|
||||
const { container } = render(
|
||||
<Wrapper>
|
||||
<Files agent_id="agent-1" />
|
||||
</Wrapper>,
|
||||
);
|
||||
expect(container.innerHTML).toBe('');
|
||||
});
|
||||
|
||||
it('passes provider as endpointOverride and resolved type as endpointTypeOverride', () => {
|
||||
mockFileConfig = mergeFileConfig({ endpoints: { default: { fileLimit: 10 } } });
|
||||
mockUseFileHandlingNoChatContext.mockClear();
|
||||
render(
|
||||
<Wrapper provider="Moonshot">
|
||||
<Files agent_id="agent-1" />
|
||||
</Wrapper>,
|
||||
);
|
||||
const params = mockUseFileHandlingNoChatContext.mock.calls[0][0];
|
||||
expect(params.endpointOverride).toBe('Moonshot');
|
||||
expect(params.endpointTypeOverride).toBe(EModelEndpoint.custom);
|
||||
});
|
||||
|
||||
it('falls back to agents for endpointOverride when no provider', () => {
|
||||
mockFileConfig = mergeFileConfig({ endpoints: { default: { fileLimit: 10 } } });
|
||||
mockUseFileHandlingNoChatContext.mockClear();
|
||||
render(
|
||||
<Wrapper>
|
||||
<Files agent_id="agent-1" />
|
||||
</Wrapper>,
|
||||
);
|
||||
const params = mockUseFileHandlingNoChatContext.mock.calls[0][0];
|
||||
expect(params.endpointOverride).toBe(EModelEndpoint.agents);
|
||||
expect(params.endpointTypeOverride).toBe(EModelEndpoint.agents);
|
||||
});
|
||||
|
||||
it('falls back to agents for endpointOverride when provider is empty string', () => {
|
||||
mockFileConfig = mergeFileConfig({ endpoints: { default: { fileLimit: 10 } } });
|
||||
mockUseFileHandlingNoChatContext.mockClear();
|
||||
render(
|
||||
<Wrapper provider="">
|
||||
<Files agent_id="agent-1" />
|
||||
</Wrapper>,
|
||||
);
|
||||
const params = mockUseFileHandlingNoChatContext.mock.calls[0][0];
|
||||
expect(params.endpointOverride).toBe(EModelEndpoint.agents);
|
||||
});
|
||||
|
||||
it('renders when provider has no specific config and agents config is enabled', () => {
|
||||
mockFileConfig = mergeFileConfig({
|
||||
endpoints: {
|
||||
[EModelEndpoint.agents]: { fileLimit: 20 },
|
||||
default: { fileLimit: 10 },
|
||||
},
|
||||
});
|
||||
render(
|
||||
<Wrapper provider="Moonshot">
|
||||
<Files agent_id="agent-1" />
|
||||
</Wrapper>,
|
||||
);
|
||||
expect(screen.getByText('com_assistants_code_interpreter_files')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,151 @@
|
|||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { useForm, FormProvider } from 'react-hook-form';
|
||||
import { EModelEndpoint, mergeFileConfig } from 'librechat-data-provider';
|
||||
import type { TEndpointsConfig } from 'librechat-data-provider';
|
||||
import type { AgentForm } from '~/common';
|
||||
import FileContext from '../FileContext';
|
||||
|
||||
const mockEndpointsConfig: TEndpointsConfig = {
|
||||
[EModelEndpoint.agents]: { userProvide: false, order: 1 },
|
||||
Moonshot: { type: EModelEndpoint.custom, userProvide: false, order: 9999 },
|
||||
};
|
||||
|
||||
let mockFileConfig = mergeFileConfig({ endpoints: { default: { fileLimit: 10 } } });
|
||||
|
||||
jest.mock('~/data-provider', () => ({
|
||||
useGetEndpointsQuery: () => ({ data: mockEndpointsConfig }),
|
||||
useGetFileConfig: ({ select }: { select?: (d: unknown) => unknown }) => ({
|
||||
data: select != null ? select(mockFileConfig) : mockFileConfig,
|
||||
}),
|
||||
useGetStartupConfig: () => ({ data: { sharePointFilePickerEnabled: false } }),
|
||||
}));
|
||||
|
||||
jest.mock('~/hooks', () => ({
|
||||
useAgentFileConfig: jest.requireActual('~/hooks/Agents/useAgentFileConfig').default,
|
||||
useLocalize: () => (key: string) => key,
|
||||
useLazyEffect: () => {},
|
||||
}));
|
||||
|
||||
const mockUseFileHandlingNoChatContext = jest.fn().mockReturnValue({
|
||||
handleFileChange: jest.fn(),
|
||||
});
|
||||
|
||||
jest.mock('~/hooks/Files/useFileHandling', () => ({
|
||||
useFileHandlingNoChatContext: (...args: unknown[]) => mockUseFileHandlingNoChatContext(...args),
|
||||
}));
|
||||
|
||||
jest.mock('~/hooks/Files/useSharePointFileHandling', () => ({
|
||||
useSharePointFileHandlingNoChatContext: () => ({
|
||||
handleSharePointFiles: jest.fn(),
|
||||
isProcessing: false,
|
||||
downloadProgress: 0,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('~/components/SharePoint', () => ({
|
||||
SharePointPickerDialog: () => null,
|
||||
}));
|
||||
|
||||
jest.mock('~/components/Chat/Input/Files/FileRow', () => () => null);
|
||||
|
||||
jest.mock('@ariakit/react', () => ({
|
||||
MenuButton: ({ children, ...props }: { children: React.ReactNode }) => (
|
||||
<button {...props}>{children}</button>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('@librechat/client', () => ({
|
||||
HoverCard: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
DropdownPopup: () => null,
|
||||
AttachmentIcon: () => <span />,
|
||||
CircleHelpIcon: () => <span />,
|
||||
SharePointIcon: () => <span />,
|
||||
HoverCardPortal: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
HoverCardContent: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
HoverCardTrigger: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
}));
|
||||
|
||||
function Wrapper({ provider, children }: { provider?: string; children: React.ReactNode }) {
|
||||
const methods = useForm<AgentForm>({
|
||||
defaultValues: { provider: provider as AgentForm['provider'] },
|
||||
});
|
||||
return <FormProvider {...methods}>{children}</FormProvider>;
|
||||
}
|
||||
|
||||
describe('FileContext', () => {
|
||||
it('renders upload UI when file uploads are not disabled', () => {
|
||||
mockFileConfig = mergeFileConfig({ endpoints: { default: { fileLimit: 10 } } });
|
||||
render(
|
||||
<Wrapper provider="Moonshot">
|
||||
<FileContext agent_id="agent-1" />
|
||||
</Wrapper>,
|
||||
);
|
||||
expect(screen.getByText('com_agents_file_context_label')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('returns null when file config is disabled', () => {
|
||||
mockFileConfig = mergeFileConfig({
|
||||
endpoints: { Moonshot: { disabled: true }, default: { fileLimit: 10 } },
|
||||
});
|
||||
const { container } = render(
|
||||
<Wrapper provider="Moonshot">
|
||||
<FileContext agent_id="agent-1" />
|
||||
</Wrapper>,
|
||||
);
|
||||
expect(container.innerHTML).toBe('');
|
||||
});
|
||||
|
||||
it('returns null when agents endpoint config is disabled and provider has no specific config', () => {
|
||||
mockFileConfig = mergeFileConfig({
|
||||
endpoints: { [EModelEndpoint.agents]: { disabled: true }, default: { fileLimit: 10 } },
|
||||
});
|
||||
const { container } = render(
|
||||
<Wrapper>
|
||||
<FileContext agent_id="agent-1" />
|
||||
</Wrapper>,
|
||||
);
|
||||
expect(container.innerHTML).toBe('');
|
||||
});
|
||||
|
||||
it('passes provider as endpointOverride and resolved type as endpointTypeOverride', () => {
|
||||
mockFileConfig = mergeFileConfig({ endpoints: { default: { fileLimit: 10 } } });
|
||||
mockUseFileHandlingNoChatContext.mockClear();
|
||||
render(
|
||||
<Wrapper provider="Moonshot">
|
||||
<FileContext agent_id="agent-1" />
|
||||
</Wrapper>,
|
||||
);
|
||||
const params = mockUseFileHandlingNoChatContext.mock.calls[0][0];
|
||||
expect(params.endpointOverride).toBe('Moonshot');
|
||||
expect(params.endpointTypeOverride).toBe(EModelEndpoint.custom);
|
||||
});
|
||||
|
||||
it('falls back to agents for endpointOverride when no provider', () => {
|
||||
mockFileConfig = mergeFileConfig({ endpoints: { default: { fileLimit: 10 } } });
|
||||
mockUseFileHandlingNoChatContext.mockClear();
|
||||
render(
|
||||
<Wrapper>
|
||||
<FileContext agent_id="agent-1" />
|
||||
</Wrapper>,
|
||||
);
|
||||
const params = mockUseFileHandlingNoChatContext.mock.calls[0][0];
|
||||
expect(params.endpointOverride).toBe(EModelEndpoint.agents);
|
||||
expect(params.endpointTypeOverride).toBe(EModelEndpoint.agents);
|
||||
});
|
||||
|
||||
it('renders when provider has no specific config and agents config is enabled', () => {
|
||||
mockFileConfig = mergeFileConfig({
|
||||
endpoints: {
|
||||
[EModelEndpoint.agents]: { fileLimit: 20 },
|
||||
default: { fileLimit: 10 },
|
||||
},
|
||||
});
|
||||
render(
|
||||
<Wrapper provider="Moonshot">
|
||||
<FileContext agent_id="agent-1" />
|
||||
</Wrapper>,
|
||||
);
|
||||
expect(screen.getByText('com_agents_file_context_label')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { useForm, FormProvider } from 'react-hook-form';
|
||||
import { EModelEndpoint, mergeFileConfig } from 'librechat-data-provider';
|
||||
import type { TEndpointsConfig } from 'librechat-data-provider';
|
||||
import type { AgentForm } from '~/common';
|
||||
import FileSearch from '../FileSearch';
|
||||
|
||||
const mockEndpointsConfig: TEndpointsConfig = {
|
||||
[EModelEndpoint.agents]: { userProvide: false, order: 1 },
|
||||
Moonshot: { type: EModelEndpoint.custom, userProvide: false, order: 9999 },
|
||||
};
|
||||
|
||||
let mockFileConfig = mergeFileConfig({ endpoints: { default: { fileLimit: 10 } } });
|
||||
|
||||
jest.mock('~/data-provider', () => ({
|
||||
useGetEndpointsQuery: () => ({ data: mockEndpointsConfig }),
|
||||
useGetFileConfig: ({ select }: { select?: (d: unknown) => unknown }) => ({
|
||||
data: select != null ? select(mockFileConfig) : mockFileConfig,
|
||||
}),
|
||||
useGetStartupConfig: () => ({ data: { sharePointFilePickerEnabled: false } }),
|
||||
}));
|
||||
|
||||
jest.mock('~/hooks', () => ({
|
||||
useAgentFileConfig: jest.requireActual('~/hooks/Agents/useAgentFileConfig').default,
|
||||
useLocalize: () => (key: string) => key,
|
||||
useLazyEffect: () => {},
|
||||
}));
|
||||
|
||||
const mockUseFileHandlingNoChatContext = jest.fn().mockReturnValue({
|
||||
handleFileChange: jest.fn(),
|
||||
});
|
||||
|
||||
jest.mock('~/hooks/Files/useFileHandling', () => ({
|
||||
useFileHandlingNoChatContext: (...args: unknown[]) => mockUseFileHandlingNoChatContext(...args),
|
||||
}));
|
||||
|
||||
jest.mock('~/hooks/Files/useSharePointFileHandling', () => ({
|
||||
useSharePointFileHandlingNoChatContext: () => ({
|
||||
handleSharePointFiles: jest.fn(),
|
||||
isProcessing: false,
|
||||
downloadProgress: 0,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('~/components/SharePoint', () => ({
|
||||
SharePointPickerDialog: () => null,
|
||||
}));
|
||||
|
||||
jest.mock('~/components/Chat/Input/Files/FileRow', () => () => null);
|
||||
jest.mock('../FileSearchCheckbox', () => () => null);
|
||||
|
||||
jest.mock('@ariakit/react', () => ({
|
||||
MenuButton: ({ children, ...props }: { children: React.ReactNode }) => (
|
||||
<button {...props}>{children}</button>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('@librechat/client', () => ({
|
||||
SharePointIcon: () => <span />,
|
||||
AttachmentIcon: () => <span />,
|
||||
DropdownPopup: () => null,
|
||||
}));
|
||||
|
||||
function Wrapper({ provider, children }: { provider?: string; children: React.ReactNode }) {
|
||||
const methods = useForm<AgentForm>({
|
||||
defaultValues: { provider: provider as AgentForm['provider'] },
|
||||
});
|
||||
return <FormProvider {...methods}>{children}</FormProvider>;
|
||||
}
|
||||
|
||||
describe('FileSearch', () => {
|
||||
it('renders upload UI when file uploads are not disabled', () => {
|
||||
mockFileConfig = mergeFileConfig({ endpoints: { default: { fileLimit: 10 } } });
|
||||
render(
|
||||
<Wrapper provider="Moonshot">
|
||||
<FileSearch agent_id="agent-1" />
|
||||
</Wrapper>,
|
||||
);
|
||||
expect(screen.getByText('com_assistants_file_search')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('returns null when file config is disabled for provider', () => {
|
||||
mockFileConfig = mergeFileConfig({
|
||||
endpoints: { Moonshot: { disabled: true }, default: { fileLimit: 10 } },
|
||||
});
|
||||
const { container } = render(
|
||||
<Wrapper provider="Moonshot">
|
||||
<FileSearch agent_id="agent-1" />
|
||||
</Wrapper>,
|
||||
);
|
||||
expect(container.innerHTML).toBe('');
|
||||
});
|
||||
|
||||
it('returns null when agents endpoint config is disabled and no provider config', () => {
|
||||
mockFileConfig = mergeFileConfig({
|
||||
endpoints: { [EModelEndpoint.agents]: { disabled: true }, default: { fileLimit: 10 } },
|
||||
});
|
||||
const { container } = render(
|
||||
<Wrapper>
|
||||
<FileSearch agent_id="agent-1" />
|
||||
</Wrapper>,
|
||||
);
|
||||
expect(container.innerHTML).toBe('');
|
||||
});
|
||||
|
||||
it('passes provider as endpointOverride and resolved type as endpointTypeOverride', () => {
|
||||
mockFileConfig = mergeFileConfig({ endpoints: { default: { fileLimit: 10 } } });
|
||||
mockUseFileHandlingNoChatContext.mockClear();
|
||||
render(
|
||||
<Wrapper provider="Moonshot">
|
||||
<FileSearch agent_id="agent-1" />
|
||||
</Wrapper>,
|
||||
);
|
||||
const params = mockUseFileHandlingNoChatContext.mock.calls[0][0];
|
||||
expect(params.endpointOverride).toBe('Moonshot');
|
||||
expect(params.endpointTypeOverride).toBe(EModelEndpoint.custom);
|
||||
});
|
||||
|
||||
it('falls back to agents for endpointOverride when no provider', () => {
|
||||
mockFileConfig = mergeFileConfig({ endpoints: { default: { fileLimit: 10 } } });
|
||||
mockUseFileHandlingNoChatContext.mockClear();
|
||||
render(
|
||||
<Wrapper>
|
||||
<FileSearch agent_id="agent-1" />
|
||||
</Wrapper>,
|
||||
);
|
||||
const params = mockUseFileHandlingNoChatContext.mock.calls[0][0];
|
||||
expect(params.endpointOverride).toBe(EModelEndpoint.agents);
|
||||
expect(params.endpointTypeOverride).toBe(EModelEndpoint.agents);
|
||||
});
|
||||
|
||||
it('renders when provider has no specific config and agents config is enabled', () => {
|
||||
mockFileConfig = mergeFileConfig({
|
||||
endpoints: {
|
||||
[EModelEndpoint.agents]: { fileLimit: 20 },
|
||||
default: { fileLimit: 10 },
|
||||
},
|
||||
});
|
||||
render(
|
||||
<Wrapper provider="Moonshot">
|
||||
<FileSearch agent_id="agent-1" />
|
||||
</Wrapper>,
|
||||
);
|
||||
expect(screen.getByText('com_assistants_file_search')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue