Skip to main content
Glama
bot.test.ts18.3 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import { allOk } from '@medplum/core'; import type { Bot } from '@medplum/fhirtypes'; import { MockClient } from '@medplum/mock'; import { randomUUID } from 'node:crypto'; import fs from 'node:fs'; import * as cli from '.'; import { createMedplumClient } from './util/client'; const { main } = cli; jest.mock('./util/client'); jest.mock('node:fs', () => ({ existsSync: jest.fn(), readFileSync: jest.fn(), writeFileSync: jest.fn(), constants: { O_CREAT: 0, }, promises: { readFile: jest.fn(async () => '{}'), }, })); describe('CLI Bots', () => { const env = process.env; let medplum: MockClient; let processError: jest.SpyInstance; beforeAll(() => { process.exit = jest.fn<never, any>().mockImplementation(function exit(exitCode: number) { throw new Error(`Process exited with exit code ${exitCode}`); }) as unknown as typeof process.exit; processError = jest.spyOn(process.stderr, 'write').mockImplementation(jest.fn()); }); beforeEach(() => { jest.resetModules(); jest.clearAllMocks(); process.env = { ...env }; medplum = new MockClient(); console.log = jest.fn(); (createMedplumClient as unknown as jest.Mock).mockImplementation(async () => medplum); }); afterEach(() => { process.env = env; }); test('Deploy bot missing name', async () => { await expect(main(['node', 'index.js', 'bot', 'deploy'])).rejects.toThrow('Process exited with exit code 1'); expect(processError).toHaveBeenNthCalledWith( 1, expect.stringContaining("error: missing required argument 'botName'") ); }); test('Deploy bot config not found', async () => { const id = randomUUID(); // Setup bot config (fs.existsSync as unknown as jest.Mock).mockReturnValue(true); (fs.readFileSync as unknown as jest.Mock).mockReturnValue( JSON.stringify({ bots: [ { name: 'hello-world', id: id, source: 'src/hello-world.ts', dist: 'dist/hello-world.js', }, ], }) ); await expect(main(['node', 'index.js', 'bot', 'deprecate', 'does-not-exist'])).rejects.toThrow( 'Process exited with exit code 1' ); expect(processError).toHaveBeenCalledWith(expect.stringContaining(`error: unknown command 'deprecate'`)); }); test('Deploy bot not found', async () => { const id = randomUUID(); // Setup bot config (fs.existsSync as unknown as jest.Mock).mockReturnValue(true); (fs.readFileSync as unknown as jest.Mock).mockReturnValue( JSON.stringify({ bots: [ { name: 'hello-world', id: id, source: 'src/hello-world.ts', dist: 'dist/hello-world.js', }, ], }) ); await expect(main(['node', 'index.js', 'bot', 'deploy', 'hello-world'])).rejects.toThrow( 'Process exited with exit code 1' ); expect(processError).toHaveBeenCalledWith( expect.stringContaining('Error: 1 bot(s) had failures. Bots with failures:') ); }); test('Save bot success', async () => { // Create the bot const bot = await medplum.createResource<Bot>({ resourceType: 'Bot' }); expect(bot.code).toBeUndefined(); // Setup bot config (fs.existsSync as unknown as jest.Mock).mockReturnValue(true); (fs.readFileSync as unknown as jest.Mock).mockReturnValue( JSON.stringify({ bots: [ { name: 'hello-world', id: bot.id, source: 'src/hello-world.ts', dist: 'dist/hello-world.js', }, ], }) ); await main(['node', 'index.js', 'bot', 'save', 'hello-world']); expect(console.log).toHaveBeenCalledWith(expect.stringMatching(/Success/)); const check = await medplum.readResource('Bot', bot.id); expect(check.code).toBeUndefined(); expect(check.sourceCode).toBeDefined(); }); test('Deploy bot success', async () => { medplum.router.router.add('POST', 'Bot/:id/$deploy', async () => [allOk]); // Create the bot const bot = await medplum.createResource<Bot>({ id: randomUUID(), resourceType: 'Bot' }); expect(bot.code).toBeUndefined(); // Setup bot config (fs.existsSync as unknown as jest.Mock).mockReturnValue(true); (fs.readFileSync as unknown as jest.Mock).mockReturnValue( JSON.stringify({ bots: [ { name: 'hello-world', id: bot.id, source: 'src/hello-world.ts', dist: 'dist/hello-world.js', }, ], }) ); await main(['node', 'index.js', 'bot', 'deploy', 'hello-world']); expect(console.log).toHaveBeenCalledWith(expect.stringMatching(/Success/)); const check = await medplum.readResource('Bot', bot.id); expect(check.code).toBeUndefined(); expect(check.sourceCode).toBeDefined(); }); test('Deploy bot without dist success', async () => { medplum.router.router.add('POST', 'Bot/:id/$deploy', async () => [allOk]); // Create the bot const bot = await medplum.createResource<Bot>({ resourceType: 'Bot' }); expect(bot.code).toBeUndefined(); // Setup bot config (fs.existsSync as unknown as jest.Mock).mockReturnValue(true); (fs.readFileSync as unknown as jest.Mock).mockReturnValue( JSON.stringify({ bots: [ { name: 'hello-world', id: bot.id, source: 'src/hello-world.ts', }, ], }) ); await main(['node', 'index.js', 'bot', 'deploy', 'hello-world']); expect(console.log).toHaveBeenCalledWith(expect.stringMatching(/Success/)); const check = await medplum.readResource('Bot', bot.id); expect(check.code).toBeUndefined(); expect(check.sourceCode).toBeDefined(); }); test('Deploy bot for multiple bot with wildcards ', async () => { medplum.router.router.add('POST', 'Bot/:id/$deploy', async () => [allOk]); // Create the bot const bot = await medplum.createResource<Bot>({ resourceType: 'Bot' }); const bot2 = await medplum.createResource<Bot>({ resourceType: 'Bot' }); // Setup bot config (fs.existsSync as unknown as jest.Mock).mockReturnValue(true); (fs.readFileSync as unknown as jest.Mock).mockReturnValue( JSON.stringify({ bots: [ { name: 'hello-world-staging', id: bot.id, source: 'src/hello-world.ts', dist: 'dist/hello-world.js', }, { name: 'hello-world-2-staging', id: bot2.id, source: 'src/hello-world.ts', dist: 'dist/hello-world.js', }, ], }) ); await main(['node', 'index.js', 'bot', 'deploy', 'he**llo*']); expect(console.log).toHaveBeenCalledWith(expect.stringMatching(/Success/)); expect(console.log).toHaveBeenCalledWith(expect.stringMatching(/Number of bots deployed: 2/)); }); test('Deploy bot multiple bot ending with bot name that has no match', async () => { // Create the bot const bot = await medplum.createResource<Bot>({ resourceType: 'Bot' }); const bot2 = await medplum.createResource<Bot>({ resourceType: 'Bot' }); // Setup bot config (fs.existsSync as unknown as jest.Mock).mockReturnValue(true); (fs.readFileSync as unknown as jest.Mock).mockReturnValue( JSON.stringify({ bots: [ { name: 'hello-world', id: bot.id, source: 'src/hello-world.ts', dist: 'dist/hello-world.js', }, { name: 'hello-world-2', id: bot2.id, source: 'src/hello-world.ts', dist: 'dist/hello-world.js', }, ], }) ); await main(['node', 'index.js', 'bot', 'deploy', '*-staging']); expect(console.log).toHaveBeenCalledWith(expect.stringMatching(/Number of bots deployed: 0/)); }); test('Deploy bot multiple bot ending with bot name with no config', async () => { // Setup bot config (fs.existsSync as unknown as jest.Mock).mockReturnValue(false); (fs.readFileSync as unknown as jest.Mock).mockReturnValue(undefined); await main(['node', 'index.js', 'bot', 'deploy', '*-staging']); expect(console.log).toHaveBeenCalledWith(expect.stringMatching(/Number of bots deployed: 0/)); }); test('Create bot command success with existing config file', async () => { medplum.router.router.add('POST', 'Bot/:id/$deploy', async () => [allOk]); // Setup bot config (fs.existsSync as unknown as jest.Mock).mockReturnValue(true); (fs.readFileSync as unknown as jest.Mock).mockReturnValue( JSON.stringify({ bots: [], }) ); await main(['node', 'index.js', 'bot', 'create', 'test-bot', '1', 'src/hello-world.ts', 'dist/src/hello-world.ts']); expect(console.log).toHaveBeenCalledWith(expect.stringMatching('Success! Bot created:')); expect(fs.existsSync).toHaveBeenCalled(); expect(fs.readFileSync).toHaveBeenCalled(); }); test('Create bot command success without existing config file', async () => { // No bot config (fs.existsSync as unknown as jest.Mock).mockReturnValue(false); (fs.readFileSync as unknown as jest.Mock).mockReturnValue(''); await main(['node', 'index.js', 'bot', 'create', 'test-bot', '1', 'src/hello-world.ts', 'dist/src/hello-world.ts']); expect(console.log).toHaveBeenCalledWith(expect.stringMatching('Success! Bot created:')); expect(fs.existsSync).toHaveBeenCalled(); expect(fs.readFileSync).not.toHaveBeenCalled(); }); test('Create bot command with auth options', async () => { // No bot config (fs.existsSync as unknown as jest.Mock).mockReturnValue(false); (fs.readFileSync as unknown as jest.Mock).mockReturnValue(''); await main([ 'node', 'index.js', 'bot', 'create', 'test-bot', '1', 'src/hello-world.ts', 'dist/src/hello-world.ts', '--base-url', 'http://localhost:8000', '--client-id', 'test-client-id', '--client-secret', 'test-client-secret', ]); expect(console.log).toHaveBeenCalledWith(expect.stringMatching('Success! Bot created:')); expect(fs.existsSync).toHaveBeenCalled(); expect(fs.readFileSync).not.toHaveBeenCalled(); }); test('Create bot error with lack of commands', async () => { await expect(main(['node', 'index.js', 'bot', 'create', 'test-bot'])).rejects.toThrow( 'Process exited with exit code 1' ); expect(processError).toHaveBeenCalledWith(expect.stringContaining("error: missing required argument 'projectId'")); }); test('Create bot do not write to config', async () => { // No bot config (fs.existsSync as unknown as jest.Mock).mockReturnValue(false); (fs.readFileSync as unknown as jest.Mock).mockReturnValue(''); (fs.writeFileSync as unknown as jest.Mock).mockImplementation(() => {}); await main([ 'node', 'index.js', 'bot', 'create', 'test-bot', '1', 'src/hello-world.ts', 'dist/src/hello-world.ts', '--no-write-config', ]); expect(console.log).toHaveBeenCalledWith(expect.stringMatching('Success! Bot created:')); expect(fs.existsSync).toHaveBeenCalled(); expect(fs.writeFileSync).not.toHaveBeenCalled(); }); // Deprecated bot commands test('Deprecate Deploy bot missing name', async () => { await expect(main(['node', 'index.js', 'deploy-bot'])).rejects.toThrow('Process exited with exit code 1'); expect(processError).toHaveBeenCalledWith(expect.stringContaining(`error: missing required argument 'botName'`)); }); test('Deprecate Deploy bot config not found', async () => { const id = randomUUID(); // Setup bot config (fs.existsSync as unknown as jest.Mock).mockReturnValue(true); (fs.readFileSync as unknown as jest.Mock).mockReturnValue( JSON.stringify({ bots: [ { name: 'hello-world', id: id, source: 'src/hello-world.ts', dist: 'dist/hello-world.js', }, ], }) ); await main(['node', 'index.js', 'deploy-bot', 'does-not-exist']); expect(console.log).toHaveBeenCalledWith(expect.stringMatching(/Number of bots deployed: 0/)); }); test('Deprecate Deploy bot not found', async () => { const id = randomUUID(); // Setup bot config (fs.existsSync as unknown as jest.Mock).mockReturnValue(true); (fs.readFileSync as unknown as jest.Mock).mockReturnValue( JSON.stringify({ bots: [ { name: 'hello-world', id: id, source: 'src/hello-world.ts', dist: 'dist/hello-world.js', }, ], }) ); await expect(main(['node', 'index.js', 'deploy-bot', 'hello-world'])).rejects.toThrow( 'Process exited with exit code 1' ); expect(processError).toHaveBeenCalledWith( expect.stringContaining('Error: 1 bot(s) had failures. Bots with failures:') ); }); test('Deprecate Save bot success', async () => { // Create the bot const bot = await medplum.createResource<Bot>({ resourceType: 'Bot' }); expect(bot.code).toBeUndefined(); // Setup bot config (fs.existsSync as unknown as jest.Mock).mockReturnValue(true); (fs.readFileSync as unknown as jest.Mock).mockReturnValue( JSON.stringify({ bots: [ { name: 'hello-world', id: bot.id, source: 'src/hello-world.ts', dist: 'dist/hello-world.js', }, ], }) ); await main(['node', 'index.js', 'save-bot', 'hello-world']); expect(console.log).toHaveBeenCalledWith(expect.stringMatching(/Success/)); const check = await medplum.readResource('Bot', bot.id); expect(check.code).toBeUndefined(); expect(check.sourceCode).toBeDefined(); }); test('Deprecate Deploy bot success', async () => { medplum.router.router.add('POST', 'Bot/:id/$deploy', async () => [allOk]); // Create the bot const bot = await medplum.createResource<Bot>({ resourceType: 'Bot' }); expect(bot.code).toBeUndefined(); // Setup bot config (fs.existsSync as unknown as jest.Mock).mockReturnValue(true); (fs.readFileSync as unknown as jest.Mock).mockReturnValue( JSON.stringify({ bots: [ { name: 'hello-world', id: bot.id, source: 'src/hello-world.ts', dist: 'dist/hello-world.js', }, ], }) ); await main(['node', 'index.js', 'deploy-bot', 'hello-world']); expect(console.log).toHaveBeenCalledWith(expect.stringMatching(/Success/)); const check = await medplum.readResource('Bot', bot.id); expect(check.code).toBeUndefined(); expect(check.sourceCode).toBeDefined(); }); test('Deprecate Deploy bot for multiple bot with wildcards ', async () => { medplum.router.router.add('POST', 'Bot/:id/$deploy', async () => [allOk]); // Create the bot const bot = await medplum.createResource<Bot>({ resourceType: 'Bot' }); const bot2 = await medplum.createResource<Bot>({ resourceType: 'Bot' }); // Setup bot config (fs.existsSync as unknown as jest.Mock).mockReturnValue(true); (fs.readFileSync as unknown as jest.Mock).mockReturnValue( JSON.stringify({ bots: [ { name: 'hello-world-staging', id: bot.id, source: 'src/hello-world.ts', dist: 'dist/hello-world.js', }, { name: 'hello-world-2-staging', id: bot2.id, source: 'src/hello-world.ts', dist: 'dist/hello-world.js', }, ], }) ); await main(['node', 'index.js', 'deploy-bot', 'he**llo*']); expect(console.log).toHaveBeenCalledWith(expect.stringMatching(/Success/)); expect(console.log).toHaveBeenCalledWith(expect.stringMatching(/Number of bots deployed: 2/)); }); test('Deprecate Deploy bot multiple bot ending with bot name that has no match', async () => { // Create the bot const bot = await medplum.createResource<Bot>({ resourceType: 'Bot' }); const bot2 = await medplum.createResource<Bot>({ resourceType: 'Bot' }); // Setup bot config (fs.existsSync as unknown as jest.Mock).mockReturnValue(true); (fs.readFileSync as unknown as jest.Mock).mockReturnValue( JSON.stringify({ bots: [ { name: 'hello-world', id: bot.id, source: 'src/hello-world.ts', dist: 'dist/hello-world.js', }, { name: 'hello-world-2', id: bot2.id, source: 'src/hello-world.ts', dist: 'dist/hello-world.js', }, ], }) ); await main(['node', 'index.js', 'deploy-bot', '*-staging']); expect(console.log).toHaveBeenCalledWith(expect.stringMatching(/Number of bots deployed: 0/)); }); test('Deprecate Deploy bot multiple bot ending with bot name with no config', async () => { // Setup bot config (fs.existsSync as unknown as jest.Mock).mockReturnValue(false); (fs.readFileSync as unknown as jest.Mock).mockReturnValue(undefined); await main(['node', 'index.js', 'deploy-bot', '*-staging']); expect(console.log).not.toHaveBeenCalledWith(expect.stringMatching(/Success/)); expect(console.log).toHaveBeenCalledWith(expect.stringMatching(/Number of bots deployed: 0/)); }); test('Deprecate Create bot command success', async () => { await main(['node', 'index.js', 'create-bot', 'test-bot', '1', 'src/hello-world.ts', 'dist/src/hello-world.ts']); expect(console.log).toHaveBeenCalledWith(expect.stringMatching('Success! Bot created:')); }); test('Deprecate Create bot error with lack of commands', async () => { await expect(main(['node', 'index.js', 'create-bot', 'test-bot'])).rejects.toThrow( 'Process exited with exit code 1' ); expect(processError).toHaveBeenCalledWith(expect.stringContaining("error: missing required argument 'projectId'")); }); });

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