From 8773b36eec7f0cbbcba9f1873de1a0707a6bfedb Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Mon, 24 Aug 2026 03:21:48 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=BD=20fix:=20Commit=20Subagent=20Roste?= =?UTF-8?q?r=20Selections=20to=20Form=20State=20(#15154)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: persist subagent selections synchronously * test: verify subagent roster form state * style: sort subagent roster test imports --- .../Agents/Advanced/AgentSubagents.tsx | 26 +++-- .../__tests__/AgentSubagents.spec.tsx | 105 ++++++++++++++++++ 2 files changed, 120 insertions(+), 11 deletions(-) create mode 100644 client/src/components/SidePanel/Agents/Advanced/__tests__/AgentSubagents.spec.tsx diff --git a/client/src/components/SidePanel/Agents/Advanced/AgentSubagents.tsx b/client/src/components/SidePanel/Agents/Advanced/AgentSubagents.tsx index f85ab7d732..98c2331eb5 100644 --- a/client/src/components/SidePanel/Agents/Advanced/AgentSubagents.tsx +++ b/client/src/components/SidePanel/Agents/Advanced/AgentSubagents.tsx @@ -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 = ({ 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 = ({ 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 = ({ field, currentAgentId, {agentIds.length < maxSubagents && ( diff --git a/client/src/components/SidePanel/Agents/Advanced/__tests__/AgentSubagents.spec.tsx b/client/src/components/SidePanel/Agents/Advanced/__tests__/AgentSubagents.spec.tsx new file mode 100644 index 0000000000..8280fc2a64 --- /dev/null +++ b/client/src/components/SidePanel/Agents/Advanced/__tests__/AgentSubagents.spec.tsx @@ -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['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({ + defaultValues: { + subagents: { + enabled: true, + allowSelf: false, + agent_ids: [], + }, + }, + }); + mockGetValues = methods.getValues; + + return ( +
+ ( + + )} + /> + + ); + } + + it('commits a selected agent before an immediate form submit', async () => { + render(); + + 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(), + ), + ); + }); +});