From 8facb40d0511747c535f69bd066bb2c6489d483e Mon Sep 17 00:00:00 2001 From: Amed Rodriguez Date: Fri, 3 Oct 2025 12:05:16 -0700 Subject: [PATCH 1/3] Fix headers values sent --- .../extensions/modal/ExtensionModal.test.tsx | 93 +++++++++++++++++++ .../extensions/modal/ExtensionModal.tsx | 34 +++++-- .../extensions/modal/HeadersSection.tsx | 11 ++- 3 files changed, 129 insertions(+), 9 deletions(-) create mode 100644 ui/desktop/src/components/settings/extensions/modal/ExtensionModal.test.tsx diff --git a/ui/desktop/src/components/settings/extensions/modal/ExtensionModal.test.tsx b/ui/desktop/src/components/settings/extensions/modal/ExtensionModal.test.tsx new file mode 100644 index 000000000000..4ed31241d6b6 --- /dev/null +++ b/ui/desktop/src/components/settings/extensions/modal/ExtensionModal.test.tsx @@ -0,0 +1,93 @@ +import { describe, it, expect, vi } from 'vitest'; +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import ExtensionModal from './ExtensionModal'; +import { ExtensionFormData } from '../utils'; + +vi.mock('../../../../api', () => ({ + upsertConfig: vi.fn().mockResolvedValue(undefined), +})); + +describe('ExtensionModal', () => { + it('creates a http_streamable extension', async () => { + const user = userEvent.setup(); + const mockOnSubmit = vi.fn(); + const mockOnClose = vi.fn(); + + const initialData: ExtensionFormData = { + name: '', + description: '', + type: 'stdio', // Default type + cmd: '', + endpoint: '', + enabled: true, + timeout: 300, + envVars: [], + headers: [], + }; + + render( + + ); + + const nameInput = screen.getByPlaceholderText('Enter extension name...'); + const submitButton = screen.getByTestId('extension-submit-btn'); + + await user.type(nameInput, 'Test MCP'); + + const typeSelect = screen.getByRole('combobox'); + await user.click(typeSelect); + + const httpOption = screen.getByText('Streamable HTTP'); + await user.click(httpOption); + + await waitFor(() => { + expect(screen.getByText('Request Headers')).toBeInTheDocument(); + }); + + const endpointInput = screen.getByPlaceholderText('Enter endpoint URL...'); + await user.type(endpointInput, 'https://foo.bar.com/mcp/'); + + const descriptionInput = screen.getByPlaceholderText('Optional description...'); + await user.type(descriptionInput, 'Test MCP extension'); + + const headerNameInput = screen.getByPlaceholderText('Header name'); + const headerValueInput = screen + .getAllByPlaceholderText('Value') + .find( + (input) => + input.closest('div')?.textContent?.includes('Request Headers') || + input.parentElement?.parentElement?.textContent?.includes('Request Headers') + ); + + await user.type(headerNameInput, 'Authorization'); + if (headerValueInput) { + await user.type(headerValueInput, 'Bearer abc123'); + } + + await user.click(submitButton); + + await waitFor(() => { + expect(mockOnSubmit).toHaveBeenCalled(); + }); + + const submittedData = mockOnSubmit.mock.calls[0][0]; + + expect(submittedData.name).toBe('Test MCP'); + expect(submittedData.type).toBe('streamable_http'); + expect(submittedData.endpoint).toBe('https://foo.bar.com/mcp/'); + expect(submittedData.description).toBe('Test MCP extension'); + expect(submittedData.timeout).toBe(300); + expect(submittedData.headers).toHaveLength(1); + expect(submittedData.headers).toEqual([ + { key: 'Authorization', value: 'Bearer abc123', isEdited: true }, + ]); + }); +}); diff --git a/ui/desktop/src/components/settings/extensions/modal/ExtensionModal.tsx b/ui/desktop/src/components/settings/extensions/modal/ExtensionModal.tsx index 3a7c84f658f4..56f277d57eff 100644 --- a/ui/desktop/src/components/settings/extensions/modal/ExtensionModal.tsx +++ b/ui/desktop/src/components/settings/extensions/modal/ExtensionModal.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useState, useCallback } from 'react'; import { Button } from '../../../ui/button'; import { Dialog, @@ -43,6 +43,7 @@ export default function ExtensionModal({ const [showCloseConfirmation, setShowCloseConfirmation] = useState(false); const [hasPendingEnvVars, setHasPendingEnvVars] = useState(false); const [hasPendingHeaders, setHasPendingHeaders] = useState(false); + const [pendingHeader, setPendingHeader] = useState<{ key: string; value: string } | null>(null); // Function to check if form has been modified const hasFormChanges = (): boolean => { @@ -72,7 +73,7 @@ export default function ExtensionModal({ envVar.value !== '••••••••' ); - // Check if there are pending environment variables being typed + // Check if there are pending environment variables or headers being typed const hasPendingInput = hasPendingEnvVars || hasPendingHeaders; return ( @@ -168,6 +169,14 @@ export default function ExtensionModal({ }); }; + const handlePendingHeaderChange = useCallback( + (hasPending: boolean, header?: { key: string; value: string }) => { + setHasPendingHeaders(hasPending); + setPendingHeader(header || null); + }, + [] + ); + // Function to store a secret value const storeSecret = async (key: string, value: string) => { try { @@ -248,9 +257,20 @@ export default function ExtensionModal({ const handleSubmit = async () => { setSubmitAttempted(true); + // Build final data with pending header included + const finalHeaders = [...formData.headers]; + if (pendingHeader) { + finalHeaders.push({ ...pendingHeader, isEdited: true }); + } + + const finalFormData = { + ...formData, + headers: finalHeaders, + }; + if (isFormValid()) { // Only store env vars that have been edited (which includes new) - const secretPromises = formData.envVars + const secretPromises = finalFormData.envVars .filter((envVar) => envVar.isEdited) .map(({ key, value }) => storeSecret(key, value)); @@ -261,9 +281,11 @@ export default function ExtensionModal({ if (results.every((success) => success)) { // Convert timeout to number if needed const dataToSubmit = { - ...formData, + ...finalFormData, timeout: - typeof formData.timeout === 'string' ? Number(formData.timeout) : formData.timeout, + typeof finalFormData.timeout === 'string' + ? Number(finalFormData.timeout) + : finalFormData.timeout, }; onSubmit(dataToSubmit); onClose(); @@ -366,7 +388,7 @@ export default function ExtensionModal({ onRemove={handleRemoveHeader} onChange={handleHeaderChange} submitAttempted={submitAttempted} - onPendingInputChange={setHasPendingHeaders} + onPendingInputChange={handlePendingHeaderChange} /> diff --git a/ui/desktop/src/components/settings/extensions/modal/HeadersSection.tsx b/ui/desktop/src/components/settings/extensions/modal/HeadersSection.tsx index 5fe28dc1c181..64005edb0c8f 100644 --- a/ui/desktop/src/components/settings/extensions/modal/HeadersSection.tsx +++ b/ui/desktop/src/components/settings/extensions/modal/HeadersSection.tsx @@ -10,7 +10,10 @@ interface HeadersSectionProps { onRemove: (index: number) => void; onChange: (index: number, field: 'key' | 'value', value: string) => void; submitAttempted: boolean; - onPendingInputChange?: (hasPending: boolean) => void; + onPendingInputChange?: ( + hasPendingInput: boolean, + pendingHeader?: { key: string; value: string } + ) => void; } export default function HeadersSection({ @@ -29,10 +32,12 @@ export default function HeadersSection({ value: false, }); - // Track pending input changes + // Notify parent when pending input changes React.useEffect(() => { const hasPendingInput = newKey.trim() !== '' || newValue.trim() !== ''; - onPendingInputChange?.(hasPendingInput); + const pendingHeader = + newKey.trim() && newValue.trim() ? { key: newKey, value: newValue } : undefined; + onPendingInputChange?.(hasPendingInput, pendingHeader); }, [newKey, newValue, onPendingInputChange]); const handleAdd = () => { From bdb0086a3b80353a9b8225a4c50c1f1c8aab91c6 Mon Sep 17 00:00:00 2001 From: Amed Rodriguez Date: Fri, 3 Oct 2025 13:26:50 -0700 Subject: [PATCH 2/3] remove mock --- .../settings/extensions/modal/ExtensionModal.test.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/ui/desktop/src/components/settings/extensions/modal/ExtensionModal.test.tsx b/ui/desktop/src/components/settings/extensions/modal/ExtensionModal.test.tsx index 4ed31241d6b6..377bc56b7d18 100644 --- a/ui/desktop/src/components/settings/extensions/modal/ExtensionModal.test.tsx +++ b/ui/desktop/src/components/settings/extensions/modal/ExtensionModal.test.tsx @@ -4,10 +4,6 @@ import userEvent from '@testing-library/user-event'; import ExtensionModal from './ExtensionModal'; import { ExtensionFormData } from '../utils'; -vi.mock('../../../../api', () => ({ - upsertConfig: vi.fn().mockResolvedValue(undefined), -})); - describe('ExtensionModal', () => { it('creates a http_streamable extension', async () => { const user = userEvent.setup(); From 6d6068a70d445fd65dc4edcc11596873c22e70cf Mon Sep 17 00:00:00 2001 From: Amed Rodriguez Date: Fri, 3 Oct 2025 14:44:59 -0700 Subject: [PATCH 3/3] feedback --- .../extensions/modal/ExtensionModal.tsx | 30 ++++++++++--------- .../extensions/modal/HeadersSection.tsx | 8 ++--- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/ui/desktop/src/components/settings/extensions/modal/ExtensionModal.tsx b/ui/desktop/src/components/settings/extensions/modal/ExtensionModal.tsx index 56f277d57eff..788cd08d3ae4 100644 --- a/ui/desktop/src/components/settings/extensions/modal/ExtensionModal.tsx +++ b/ui/desktop/src/components/settings/extensions/modal/ExtensionModal.tsx @@ -170,9 +170,9 @@ export default function ExtensionModal({ }; const handlePendingHeaderChange = useCallback( - (hasPending: boolean, header?: { key: string; value: string }) => { + (hasPending: boolean, header: { key: string; value: string } | null) => { setHasPendingHeaders(hasPending); - setPendingHeader(header || null); + setPendingHeader(header); }, [] ); @@ -226,8 +226,16 @@ export default function ExtensionModal({ ); }; + const getFinalHeaders = () => { + const finalHeaders = [...formData.headers]; + if (pendingHeader && pendingHeader.key.trim() !== '' && pendingHeader.value.trim() !== '') { + finalHeaders.push({ ...pendingHeader, isEdited: true }); + } + return finalHeaders; + }; + const isHeadersValid = () => { - return formData.headers.every( + return getFinalHeaders().every( ({ key, value }) => (key === '' && value === '') || (key !== '' && value !== '') ); }; @@ -257,18 +265,12 @@ export default function ExtensionModal({ const handleSubmit = async () => { setSubmitAttempted(true); - // Build final data with pending header included - const finalHeaders = [...formData.headers]; - if (pendingHeader) { - finalHeaders.push({ ...pendingHeader, isEdited: true }); - } - - const finalFormData = { - ...formData, - headers: finalHeaders, - }; - if (isFormValid()) { + const finalFormData = { + ...formData, + headers: getFinalHeaders(), + }; + // Only store env vars that have been edited (which includes new) const secretPromises = finalFormData.envVars .filter((envVar) => envVar.isEdited) diff --git a/ui/desktop/src/components/settings/extensions/modal/HeadersSection.tsx b/ui/desktop/src/components/settings/extensions/modal/HeadersSection.tsx index 64005edb0c8f..f7ea5037b5b8 100644 --- a/ui/desktop/src/components/settings/extensions/modal/HeadersSection.tsx +++ b/ui/desktop/src/components/settings/extensions/modal/HeadersSection.tsx @@ -10,9 +10,9 @@ interface HeadersSectionProps { onRemove: (index: number) => void; onChange: (index: number, field: 'key' | 'value', value: string) => void; submitAttempted: boolean; - onPendingInputChange?: ( + onPendingInputChange: ( hasPendingInput: boolean, - pendingHeader?: { key: string; value: string } + pendingHeader: { key: string; value: string } | null ) => void; } @@ -36,8 +36,8 @@ export default function HeadersSection({ React.useEffect(() => { const hasPendingInput = newKey.trim() !== '' || newValue.trim() !== ''; const pendingHeader = - newKey.trim() && newValue.trim() ? { key: newKey, value: newValue } : undefined; - onPendingInputChange?.(hasPendingInput, pendingHeader); + newKey.trim() && newValue.trim() ? { key: newKey, value: newValue } : null; + onPendingInputChange(hasPendingInput, pendingHeader); }, [newKey, newValue, onPendingInputChange]); const handleAdd = () => {