Skip to main content
Glama
ToolsPage.test.tsx20.4 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import { cleanNotifications } from '@mantine/notifications'; import type { LogMessage } from '@medplum/core'; import { ContentType, MEDPLUM_RELEASES_URL, MEDPLUM_VERSION, allOk, clearReleaseCache, getReferenceString, getStatus, isOperationOutcome, serverError, sleep, } from '@medplum/core'; import type { Agent } from '@medplum/fhirtypes'; import { MockClient } from '@medplum/mock'; import type { ReactNode } from 'react'; import { act, fireEvent, renderAppRoutes, screen } from '../test-utils/render'; jest.mock('react-dom', () => ({ ...jest.requireActual('react-dom'), createPortal: (children: ReactNode) => <>{children}</>, })); function mockFetch( status: number, body: Record<string, unknown> | ((url: string, options?: any) => any), contentType = ContentType.JSON ): jest.Mock { const bodyFn = typeof body === 'function' ? body : () => body; return jest.fn((url: string, options?: any) => { const response = bodyFn(url, options); const responseStatus = isOperationOutcome(response) ? getStatus(response) : status; return Promise.resolve({ ok: responseStatus < 400, status: responseStatus, headers: new Headers({ 'content-type': contentType }), blob: () => Promise.resolve(response), json: () => Promise.resolve(response), }); }); } describe('ToolsPage', () => { let agent: Agent; let medplum: MockClient; function setup(url: string): void { renderAppRoutes(medplum, url); } beforeAll(async () => { medplum = new MockClient(); medplum.router.router.add('GET', 'Agent/:id/$status', async () => [ allOk, { resourceType: 'Parameters', parameter: [ { name: 'status', valueCode: 'disconnected' }, { name: 'version', valueString: MEDPLUM_VERSION }, ], }, ]); agent = await medplum.createResource<Agent>({ resourceType: 'Agent', name: 'Agente', status: 'active', } satisfies Agent); }); afterEach(() => { act(() => { cleanNotifications(); }); }); test('Get status', async () => { setup(`/${getReferenceString(agent)}/tools`); expect((await screen.findAllByText(agent.name))[0]).toBeInTheDocument(); act(() => { fireEvent.click(screen.getByText('Get Status')); }); await expect(screen.findByText('disconnected', { exact: false })).resolves.toBeInTheDocument(); expect(screen.getByText(MEDPLUM_VERSION)).toBeInTheDocument(); }); test('Renders last ping', async () => { // load agent page setup(`/${getReferenceString(agent)}`); const toolsTab = screen.getByRole('tab', { name: 'Tools' }); // click on Tools tab act(() => { fireEvent.click(toolsTab); }); expect(screen.getAllByText(agent.name)[0]).toBeInTheDocument(); act(() => { fireEvent.change(screen.getByLabelText('IP Address / Hostname'), { target: { value: '8.8.8.8' } }); fireEvent.click(screen.getByLabelText('Ping')); }); await expect(screen.findByText('statistics', { exact: false })).resolves.toBeInTheDocument(); }); test('Displays error notification whenever invalid IP entered', async () => { // load agent tools page setup(`/${getReferenceString(agent)}/tools`); expect(screen.getAllByText(agent.name)[0]).toBeInTheDocument(); act(() => { fireEvent.change(screen.getByLabelText('IP Address / Hostname'), { target: { value: 'abc123' } }); fireEvent.click(screen.getByLabelText('Ping')); }); await expect(screen.findByText('Destination device not found')).resolves.toBeInTheDocument(); }); test('Displays error notification whenever agent unreachable', async () => { medplum.setAgentAvailable(false); // load agent tools page setup(`/${getReferenceString(agent)}/tools`); expect(screen.getAllByText(agent.name)[0]).toBeInTheDocument(); act(() => { fireEvent.change(screen.getByLabelText('IP Address / Hostname'), { target: { value: '8.8.8.8' } }); fireEvent.click(screen.getByLabelText('Ping')); }); await expect(screen.findByText('Timeout')).resolves.toBeInTheDocument(); medplum.setAgentAvailable(true); }); test('Setting count for ping', async () => { const pushToAgentSpy = jest.spyOn(medplum, 'pushToAgent'); // load agent page setup(`/${getReferenceString(agent)}`); const toolsTab = screen.getByRole('tab', { name: 'Tools' }); // click on Tools tab act(() => { fireEvent.click(toolsTab); }); expect(screen.getAllByText(agent.name)[0]).toBeInTheDocument(); act(() => { fireEvent.change(screen.getByLabelText('IP Address / Hostname'), { target: { value: '8.8.8.8' } }); }); act(() => { fireEvent.change(screen.getByLabelText('Ping Count'), { target: { value: '2' } }); fireEvent.click(screen.getByLabelText('Ping')); }); await expect(screen.findByText('statistics', { exact: false })).resolves.toBeInTheDocument(); expect(pushToAgentSpy).toHaveBeenLastCalledWith( { reference: getReferenceString(agent) }, '8.8.8.8', 'PING 2', ContentType.PING, true ); pushToAgentSpy.mockRestore(); }); test('No host entered for ping', async () => { const pushToAgentSpy = jest.spyOn(medplum, 'pushToAgent'); // load agent page setup(`/${getReferenceString(agent)}`); const toolsTab = screen.getByRole('tab', { name: 'Tools' }); // click on Tools tab act(() => { fireEvent.click(toolsTab); }); expect(screen.getAllByText(agent.name)[0]).toBeInTheDocument(); act(() => { fireEvent.click(screen.getByLabelText('Ping')); }); await expect(screen.findByText('statistics', { exact: false })).rejects.toThrow(); expect(pushToAgentSpy).not.toHaveBeenCalled(); pushToAgentSpy.mockRestore(); }); test('Reload config -- Success', async () => { medplum = new MockClient(); medplum.router.router.add('GET', 'Agent/:id/$reload-config', async () => [ allOk, { resourceType: 'Parameters', parameter: [], }, ]); agent = await medplum.createResource<Agent>({ resourceType: 'Agent', name: 'Agente - Reload success', status: 'active', }); setup(`/${getReferenceString(agent)}/tools`); expect((await screen.findAllByText(agent.name))[0]).toBeInTheDocument(); act(() => { fireEvent.click(screen.getByRole('button', { name: /reload config/i })); }); await expect(screen.findByText('Success')).resolves.toBeInTheDocument(); }); test('Reload config -- Error', async () => { medplum = new MockClient(); medplum.router.router.add('GET', 'Agent/:id/$reload-config', async () => [ serverError(new Error('Something is broken')), ]); agent = await medplum.createResource<Agent>({ resourceType: 'Agent', name: 'Agente - Reload error', status: 'active', }); setup(`/${getReferenceString(agent)}/tools`); expect((await screen.findAllByText(agent.name))[0]).toBeInTheDocument(); act(() => { fireEvent.click(screen.getByRole('button', { name: /reload config/i })); }); await act(async () => { await sleep(500); }); expect(await screen.findByText(/something is broken/i)).toBeInTheDocument(); }); test('Upgrade -- Success', async () => { clearReleaseCache(); globalThis.fetch = mockFetch(200, (url) => { if (url.startsWith(`${MEDPLUM_RELEASES_URL}/latest`)) { return { tag_name: 'v3.2.14', assets: [ { url: 'https://api.github.com/repos/medplum/medplum/releases/assets/193665170', id: 193665170, name: 'medplum-agent-3.2.14-linux', browser_download_url: 'https://github.com/medplum/medplum/releases/download/v3.2.14/medplum-agent-3.2.14-linux', }, ], }; } throw new Error('Expected Github releases URL to be called'); }); medplum = new MockClient(); medplum.router.router.add('GET', 'Agent/:id/$status', async () => [ allOk, { resourceType: 'Parameters', parameter: [ { name: 'status', valueCode: 'connected', }, { name: 'version', valueString: '3.2.13', }, { name: 'lastUpdated', valueCode: new Date().toISOString(), }, ], }, ]); medplum.router.router.add('GET', 'Agent/:id/$upgrade', async () => [ allOk, { resourceType: 'Parameters', parameter: [], }, ]); agent = await medplum.createResource<Agent>({ resourceType: 'Agent', name: 'Agente - Upgrade success', status: 'active', }); setup(`/${getReferenceString(agent)}/tools`); expect((await screen.findAllByText(agent.name))[0]).toBeInTheDocument(); act(() => { fireEvent.click(screen.getByRole('button', { name: /upgrade/i })); }); // This sleep is load bearing // Basically there is some strange behavior around the Mantine Portal implementation where it is initially rendered as `null` // The theory is that the above `act` is unable to track the Modal children since they are initially not rendered and therefore their useEffects // Are not queued before the end of the `act` block // See: https://github.com/mantinedev/mantine/blob/master/packages/%40mantine/core/src/components/Portal/Portal.tsx await act(async () => { await sleep(150); }); await expect( screen.findByText('Are you sure you want to upgrade this agent from version 3.2.13 to version 3.2.14?') ).resolves.toBeInTheDocument(); act(() => { fireEvent.click(screen.getByRole('button', { name: /confirm upgrade/i })); }); await expect(screen.findByText('Success')).resolves.toBeInTheDocument(); }); test('Upgrade -- Already up-to-date', async () => { clearReleaseCache(); globalThis.fetch = mockFetch(200, (url) => { if (url.startsWith(`${MEDPLUM_RELEASES_URL}/latest`)) { return { tag_name: 'v3.2.14', assets: [ { url: 'https://api.github.com/repos/medplum/medplum/releases/assets/193665170', id: 193665170, name: 'medplum-agent-3.2.14-linux', browser_download_url: 'https://github.com/medplum/medplum/releases/download/v3.2.14/medplum-agent-3.2.14-linux', }, ], }; } throw new Error('Expected Github releases URL to be called'); }); medplum = new MockClient(); medplum.router.router.add('GET', 'Agent/:id/$status', async () => [ allOk, { resourceType: 'Parameters', parameter: [ { name: 'status', valueCode: 'connected', }, { name: 'version', valueString: '3.2.14', }, { name: 'lastUpdated', valueCode: new Date().toISOString(), }, ], }, ]); medplum.router.router.add('GET', 'Agent/:id/$upgrade', async () => [ allOk, { resourceType: 'Parameters', parameter: [], }, ]); agent = await medplum.createResource<Agent>({ resourceType: 'Agent', name: 'Agente - Upgrade up-to-date', status: 'active', }); setup(`/${getReferenceString(agent)}/tools`); expect((await screen.findAllByText(agent.name))[0]).toBeInTheDocument(); act(() => { fireEvent.click(screen.getByRole('button', { name: /upgrade/i })); }); // This sleep is load bearing // Basically there is some strange behavior around the Mantine Portal implementation where it is initially rendered as `null` // The theory is that the above `act` is unable to track the Modal children since they are initially not rendered and therefore their useEffects // Are not queued before the end of the `act` block // See: https://github.com/mantinedev/mantine/blob/master/packages/%40mantine/core/src/components/Portal/Portal.tsx await act(async () => { await sleep(150); }); await expect( screen.findByText('This agent is already on the latest version (3.2.14).') ).resolves.toBeInTheDocument(); expect(screen.queryByRole('button', { name: /confirm upgrade/i })).not.toBeInTheDocument(); }); test('Upgrade -- Unable to get version', async () => { clearReleaseCache(); globalThis.fetch = mockFetch(200, (url) => { if (url.startsWith(`${MEDPLUM_RELEASES_URL}/latest`)) { return { tag_name: 'v3.2.14', assets: [ { url: 'https://api.github.com/repos/medplum/medplum/releases/assets/193665170', id: 193665170, name: 'medplum-agent-3.2.14-linux', browser_download_url: 'https://github.com/medplum/medplum/releases/download/v3.2.14/medplum-agent-3.2.14-linux', }, ], }; } throw new Error('Expected Github releases URL to be called'); }); medplum = new MockClient(); medplum.router.router.add('GET', 'Agent/:id/$status', async () => [ allOk, { resourceType: 'Parameters', parameter: [ { name: 'status', valueCode: 'unknown', }, { name: 'version', valueString: 'unknown', }, ], }, ]); medplum.router.router.add('GET', 'Agent/:id/$upgrade', async () => [ allOk, { resourceType: 'Parameters', parameter: [], }, ]); agent = await medplum.createResource<Agent>({ resourceType: 'Agent', name: 'Agente - Upgrade unknown version', status: 'active', }); setup(`/${getReferenceString(agent)}/tools`); expect((await screen.findAllByText(agent.name))[0]).toBeInTheDocument(); act(() => { fireEvent.click(screen.getByRole('button', { name: /upgrade/i })); }); // This sleep is load bearing // Basically there is some strange behavior around the Mantine Portal implementation where it is initially rendered as `null` // The theory is that the above `act` is unable to track the Modal children since they are initially not rendered and therefore their useEffects // Are not queued before the end of the `act` block // See: https://github.com/mantinedev/mantine/blob/master/packages/%40mantine/core/src/components/Portal/Portal.tsx await act(async () => { await sleep(150); }); expect( await screen.findByText( 'Unable to determine the current version of the agent. Check the network connectivity of the agent.' ) ).toBeInTheDocument(); expect(screen.queryByRole('button', { name: /confirm upgrade/i })).not.toBeInTheDocument(); }); test('Upgrade -- Error', async () => { clearReleaseCache(); globalThis.fetch = mockFetch(200, (url) => { if (url.startsWith(`${MEDPLUM_RELEASES_URL}/latest`)) { return { tag_name: 'v3.2.14', assets: [ { url: 'https://api.github.com/repos/medplum/medplum/releases/assets/193665170', id: 193665170, name: 'medplum-agent-3.2.14-linux', browser_download_url: 'https://github.com/medplum/medplum/releases/download/v3.2.14/medplum-agent-3.2.14-linux', }, ], }; } throw new Error('Expected Github releases URL to be called'); }); medplum = new MockClient(); medplum.router.router.add('GET', 'Agent/:id/$status', async () => [ allOk, { resourceType: 'Parameters', parameter: [ { name: 'status', valueCode: 'connected', }, { name: 'version', valueString: '3.2.13', }, { name: 'lastUpdated', valueCode: new Date().toISOString(), }, ], }, ]); medplum.router.router.add('GET', 'Agent/:id/$upgrade', async () => { return [serverError(new Error('Something is broken'))]; }); agent = await medplum.createResource<Agent>({ resourceType: 'Agent', name: 'Agente - Upgrade error', status: 'active', }); setup(`/${getReferenceString(agent)}/tools`); expect((await screen.findAllByText(agent.name))[0]).toBeInTheDocument(); act(() => { fireEvent.click(screen.getByRole('button', { name: /upgrade/i })); }); // This sleep is load bearing // Basically there is some strange behavior around the Mantine Portal implementation where it is initially rendered as `null` // The theory is that the above `act` is unable to track the Modal children since they are initially not rendered and therefore their useEffects // Are not queued before the end of the `act` block // See: https://github.com/mantinedev/mantine/blob/master/packages/%40mantine/core/src/components/Portal/Portal.tsx await act(async () => { await sleep(150); }); await expect( screen.findByText('Are you sure you want to upgrade this agent from version 3.2.13 to version 3.2.14?') ).resolves.toBeInTheDocument(); const medplumGetSpy = jest.spyOn(medplum, 'get'); act(() => { fireEvent.click(screen.getByRole('button', { name: /confirm upgrade/i })); }); const upgradeUrl = medplum.fhirUrl('Agent', agent.id as string, '$upgrade'); upgradeUrl.searchParams.set('force', 'false'); expect(medplumGetSpy).toHaveBeenCalledWith(upgradeUrl, expect.objectContaining({ cache: 'reload' })); await act(async () => { await sleep(500); }); expect(await screen.findByText(/something is broken/i)).toBeInTheDocument(); }); test('Fetch logs -- Success', async () => { medplum = new MockClient(); medplum.router.router.add('GET', 'Agent/:id/$fetch-logs', async () => { return [ allOk, { resourceType: 'Parameters', parameter: [ { name: 'logs', valueString: ( [ { level: 'INFO', timestamp: new Date().toISOString(), msg: 'Test 1' }, { level: 'INFO', timestamp: new Date().toISOString(), msg: 'Test 2' }, { level: 'WARN', timestamp: new Date().toISOString(), msg: 'Test 3' }, { level: 'ERROR', timestamp: new Date().toISOString(), msg: 'There is an error', error: 'There is an error', }, ] as LogMessage[] ) .map((msg) => JSON.stringify(msg)) .join('\n'), }, ], }, ]; }); agent = await medplum.createResource<Agent>({ resourceType: 'Agent', name: 'Agente - Fetch logs success', status: 'active', }); setup(`/${getReferenceString(agent)}/tools`); expect((await screen.findAllByText(agent.name))[0]).toBeInTheDocument(); act(() => { fireEvent.click(screen.getByRole('button', { name: /fetch logs/i })); }); expect((await screen.findAllByText(/there is an error/i))[0]).toBeInTheDocument(); }); test('Fetch logs -- Error', async () => { medplum = new MockClient(); medplum.router.router.add('GET', 'Agent/:id/$fetch-logs', async () => [ serverError(new Error('Something is broken')), ]); agent = await medplum.createResource<Agent>({ resourceType: 'Agent', name: 'Agente - Fetch logs error', status: 'active', }); setup(`/${getReferenceString(agent)}/tools`); expect((await screen.findAllByText(agent.name))[0]).toBeInTheDocument(); act(() => { fireEvent.click(screen.getByRole('button', { name: /fetch logs/i })); }); await act(async () => { await sleep(500); }); expect(await screen.findByText(/something is broken/i)).toBeInTheDocument(); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/medplum/medplum'

If you have feedback or need assistance with the MCP directory API, please join our Discord server