mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-30 06:47:42 +00:00
🎽 fix: Commit Subagent Roster Selections to Form State (#15154)
* fix: persist subagent selections synchronously * test: verify subagent roster form state * style: sort subagent roster test imports
This commit is contained in:
parent
092bc583a8
commit
8773b36eec
2 changed files with 120 additions and 11 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { Switch } from '@librechat/client';
|
||||
import { Network, Users } from 'lucide-react';
|
||||
import type { ControllerRenderProps } from 'react-hook-form';
|
||||
|
|
@ -16,7 +16,6 @@ interface AgentSubagentsProps {
|
|||
|
||||
const AgentSubagents: React.FC<AgentSubagentsProps> = ({ field, currentAgentId, maxSubagents }) => {
|
||||
const localize = useLocalize();
|
||||
const [newAgentId, setNewAgentId] = useState('');
|
||||
|
||||
const fieldValue = field.value;
|
||||
const value = useMemo(() => fieldValue ?? {}, [fieldValue]);
|
||||
|
|
@ -65,14 +64,19 @@ const AgentSubagents: React.FC<AgentSubagentsProps> = ({ field, currentAgentId,
|
|||
[field, value],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (newAgentId && agentIds.length < maxSubagents && !agentIds.includes(newAgentId)) {
|
||||
setAgentIds([...agentIds, newAgentId]);
|
||||
setNewAgentId('');
|
||||
} else if (newAgentId) {
|
||||
setNewAgentId('');
|
||||
}
|
||||
}, [newAgentId, agentIds, maxSubagents, setAgentIds]);
|
||||
const addAgent = useCallback(
|
||||
(agentId: string) => {
|
||||
if (!agentId || agentIds.length >= maxSubagents || agentIds.includes(agentId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** Commit the selection directly to react-hook-form. Deferring this
|
||||
* through component state and an effect allowed an immediate form submit
|
||||
* to persist the enable toggles before the selected roster. */
|
||||
setAgentIds([...agentIds, agentId]);
|
||||
},
|
||||
[agentIds, maxSubagents, setAgentIds],
|
||||
);
|
||||
|
||||
const removeAgentAt = (index: number) => {
|
||||
setAgentIds(agentIds.filter((_, i) => i !== index));
|
||||
|
|
@ -140,7 +144,7 @@ const AgentSubagents: React.FC<AgentSubagentsProps> = ({ field, currentAgentId,
|
|||
{agentIds.length < maxSubagents && (
|
||||
<AddAgentSelect
|
||||
options={options}
|
||||
onSelect={setNewAgentId}
|
||||
onSelect={addAgent}
|
||||
placeholder={localize('com_ui_agent_subagents_add')}
|
||||
ariaLabel={localize('com_ui_agent_subagents_add')}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,105 @@
|
|||
/**
|
||||
* @jest-environment jsdom
|
||||
*/
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import type { UseFormReturn } from 'react-hook-form';
|
||||
import type { ReactNode } from 'react';
|
||||
import type { AgentForm } from '~/common';
|
||||
import AgentSubagents from '../AgentSubagents';
|
||||
|
||||
let mockSelectAgent: ((agentId: string) => void) | undefined;
|
||||
let mockGetValues: UseFormReturn<AgentForm>['getValues'] | undefined;
|
||||
const mockSubmit = jest.fn();
|
||||
|
||||
jest.mock('@librechat/client', () => ({
|
||||
Switch: () => null,
|
||||
}));
|
||||
|
||||
jest.mock('~/hooks', () => ({
|
||||
useLocalize: () => (key: string) => key,
|
||||
}));
|
||||
|
||||
jest.mock('../AgentList', () => ({
|
||||
AddAgentSelect: ({ onSelect }: { onSelect: (agentId: string) => void }) => {
|
||||
mockSelectAgent = onSelect;
|
||||
return null;
|
||||
},
|
||||
ListMeta: () => null,
|
||||
StaticAgentRow: () => null,
|
||||
useSelectableAgents: () => ({ options: [], getAgent: () => undefined }),
|
||||
}));
|
||||
|
||||
jest.mock('../OrchestrationPattern', () => ({
|
||||
__esModule: true,
|
||||
default: ({ children, trailing }: { children: ReactNode; trailing: ReactNode }) => (
|
||||
<>
|
||||
{trailing}
|
||||
{children}
|
||||
</>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('../ui', () => ({
|
||||
ToggleSetting: () => null,
|
||||
}));
|
||||
|
||||
describe('AgentSubagents', () => {
|
||||
beforeEach(() => {
|
||||
mockSelectAgent = undefined;
|
||||
mockGetValues = undefined;
|
||||
mockSubmit.mockReset();
|
||||
});
|
||||
|
||||
function Harness() {
|
||||
const methods = useForm<AgentForm>({
|
||||
defaultValues: {
|
||||
subagents: {
|
||||
enabled: true,
|
||||
allowSelf: false,
|
||||
agent_ids: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
mockGetValues = methods.getValues;
|
||||
|
||||
return (
|
||||
<form aria-label="agent form" onSubmit={methods.handleSubmit(mockSubmit)}>
|
||||
<Controller
|
||||
name="subagents"
|
||||
control={methods.control}
|
||||
render={({ field }) => (
|
||||
<AgentSubagents field={field} currentAgentId="parent" maxSubagents={10} />
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
it('commits a selected agent before an immediate form submit', async () => {
|
||||
render(<Harness />);
|
||||
|
||||
act(() => {
|
||||
mockSelectAgent?.('child');
|
||||
fireEvent.submit(screen.getByRole('form', { name: 'agent form' }));
|
||||
});
|
||||
|
||||
expect(mockGetValues?.('subagents')).toEqual({
|
||||
enabled: true,
|
||||
allowSelf: false,
|
||||
agent_ids: ['child'],
|
||||
});
|
||||
await waitFor(() =>
|
||||
expect(mockSubmit).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
subagents: {
|
||||
enabled: true,
|
||||
allowSelf: false,
|
||||
agent_ids: ['child'],
|
||||
},
|
||||
}),
|
||||
expect.anything(),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue