import * as client from '@botpress/client' import * as sdk from '@botpress/sdk' import * as fslib from 'fs' import * as pathlib from 'path' import * as uuid from 'uuid' import impl from '../../src' import * as apiUtils from '../../src/api' import * as consts from '../../src/consts' import * as cliUtils from '../../src/utils' import { ApiBot, fetchAllBots } from '../api' import defaults from '../defaults' import * as retry from '../retry' import { Test, TestProps } from '../typings' import * as utils from '../utils' const issueSchema = sdk.z.object({ id: sdk.z.string(), priority: sdk.z.enum(['high', 'medium', 'low']), title: sdk.z.string(), body: sdk.z.string(), }) const INTEGRATION = { version: '0.0.1', title: 'An Integration', description: 'An integration', user: { tags: { id: { title: 'ID', description: 'The user ID' } } }, configuration: { schema: sdk.z.object({}), identifier: { required: true, linkTemplateScript: '' } }, configurations: { token: { title: 'API Token', description: 'The token to authenticate with', schema: sdk.z.object({ token: sdk.z.string() }), }, }, actions: { getIssue: { input: { schema: sdk.z.object({ id: sdk.z.string() }) }, output: { schema: issueSchema }, }, }, events: { issueCreated: { title: 'Issue Created', description: 'An issue was created', schema: issueSchema, }, }, channels: { issueComment: { title: 'Issue Comment', description: 'Comment on an issue', messages: { text: sdk.messages.defaults.text }, conversation: { tags: { id: { title: 'ID', description: 'The issue ID' } } }, message: { tags: { id: { title: 'ID', description: 'The issue comment ID' } } }, }, }, entities: { issue: { title: 'Issue', description: 'An issue', schema: issueSchema, }, }, states: { lastCreatedIssue: { type: 'integration', schema: issueSchema, }, }, identifier: { extractScript: '', }, } satisfies Omit const getHomeDir = (props: { tmpDir: string }) => pathlib.join(props.tmpDir, '.botpresshome') const initBot = async (props: TestProps, definitionFile: string) => { const { tmpDir, dependencies, ...creds } = props const argv = { ...defaults, botpressHome: getHomeDir(props), confirm: true, ...creds, } const botName = uuid.v4().replace(/-/g, '') const botDir = pathlib.join(tmpDir, botName) await impl .init({ ...argv, workDir: tmpDir, name: botName, type: 'bot', template: 'empty' }) .then(utils.handleExitCode) await utils.fixBotpressDependencies({ workDir: botDir, target: dependencies }) await utils.npmInstall({ workDir: botDir }).then(utils.handleExitCode) await fslib.promises.writeFile(pathlib.join(botDir, 'bot.definition.ts'), definitionFile) return { botDir } } // TODO: add an equivalent test with an interface once interfaces can be created by any workspace export const addIntegration: Test = { name: 'cli should allow installing an integration', handler: async (props) => { const { tmpDir, workspaceHandle, logger, ...creds } = props const argv = { ...defaults, botpressHome: getHomeDir({ tmpDir }), confirm: true, ...creds, } const bpClient = new client.Client({ apiUrl: creds.apiUrl, token: creds.token, workspaceId: creds.workspaceId, retry: retry.config, }) const integrationSuffix = uuid.v4().replace(/-/g, '') const name = `myintegration${integrationSuffix}` const integrationName = `${workspaceHandle}/${name}` const createIntegrationBody = await apiUtils.prepareCreateIntegrationBody( new sdk.IntegrationDefinition({ ...INTEGRATION, name: integrationName, }) ) const { integration } = await bpClient.createIntegration({ ...createIntegrationBody, dev: true, // this way we ensure the integration will eventually be janitored if the test fails url: creds.apiUrl, }) try { logger.info('Initializing bot') const { botDir } = await initBot( props, [ 'import * as sdk from "@botpress/sdk"', `import anIntegration from "./bp_modules/${workspaceHandle}-${name}"`, 'export default new sdk.BotDefinition({}).addIntegration(anIntegration, {', ' enabled: true,', ' configurationType: null,', ' configuration: {},', '})', ].join('\n') ) logger.info('Logging in') await impl.login(argv).then(utils.handleExitCode) logger.info('Installing integration') await impl .add({ ...argv, installPath: botDir, packageRef: integration.id, useDev: false, alias: undefined, }) .then(utils.handleExitCode) logger.info('Building bot') await impl.build({ ...argv, workDir: botDir }).then(utils.handleExitCode) await utils.tscCheck({ workDir: botDir }).then(utils.handleExitCode) } finally { await impl.integrations .delete({ ...argv, integrationRef: integration.id, }) .catch(() => { logger.warn(`Failed to delete integration ${integration.id}`) // this is not the purpose of the test }) } }, } export const addPlugin: Test = { name: 'cli should allow installing a plugin', handler: async (props) => { const { tmpDir, workspaceHandle, logger, ...creds } = props const argv = { ...defaults, botpressHome: getHomeDir({ tmpDir }), confirm: true, ...creds, } const bpClient = new client.Client({ apiUrl: creds.apiUrl, token: creds.token, workspaceId: creds.workspaceId, retry: retry.config, }) const pluginSuffix = uuid.v4().replace(/-/g, '') const name = `myplugin${pluginSuffix}` const pluginName = `${workspaceHandle}/${name}` const pluginVersion = '0.1.0' const pluginAlias = `alias-${uuid.v4().replace(/-/g, '')}` const botName = uuid.v4() const createPluginBody = await apiUtils.prepareCreatePluginBody( new sdk.PluginDefinition({ name: pluginName, version: pluginVersion, }) ) const { plugin } = await bpClient.createPlugin({ ...createPluginBody, code: { browser: 'export default {}', node: 'export default {}' }, }) let bot: ApiBot | undefined try { logger.info('Initializing bot') const { botDir } = await initBot( props, [ 'import * as sdk from "@botpress/sdk"', `import aPlugin from "./bp_modules/${workspaceHandle}-${name}"`, 'export default new sdk.BotDefinition({}).addPlugin(aPlugin, {', ` alias: '${pluginAlias}',`, ' configuration: {},', ' dependencies: {},', '})', ].join('\n') ) logger.info('Logging in') await impl.login(argv).then(utils.handleExitCode) logger.info('Installing plugin') await impl .add({ ...argv, installPath: botDir, packageRef: plugin.id, useDev: false, alias: undefined, }) .then(utils.handleExitCode) logger.info('Building bot') await impl.build({ ...argv, workDir: botDir }).then(utils.handleExitCode) await utils.tscCheck({ workDir: botDir }).then(utils.handleExitCode) logger.info('Deploying bot') await impl.bots.create({ ...argv, name: botName, ifNotExists: false }).then(utils.handleExitCode) bot = await fetchBot(bpClient, botName) if (!bot) { throw new Error(`Bot ${botName} should have been created`) } await impl.deploy({ ...argv, workDir: botDir, createNewBot: false, botId: bot.id }).then(utils.handleExitCode) logger.info('Checking if plugin is installed') const plugins = await bpClient.getBot({ id: bot.id }).then(({ bot }) => bot.plugins) const isPluginInstalled = Object.entries(plugins).some( ([alias, instance]) => alias === pluginAlias && instance.id === plugin.id ) if (!isPluginInstalled) { throw new Error(`Plugin ${plugin.id} should have been installed in bot ${bot.id}`) } } finally { if (bot) { await impl.bots .delete({ ...argv, botRef: bot.id, }) .catch(() => { logger.warn(`Failed to delete bot ${bot!.id}`) // this is not the purpose of the test }) } await impl.plugins .delete({ ...argv, pluginRef: plugin.id, }) .catch(() => { logger.warn(`Failed to delete plugin ${plugin.id}`) // this is not the purpose of the test }) } }, } const fetchBot = async (client: client.Client, botName: string): Promise => await fetchAllBots(client).then((bots) => bots.find(({ name }) => name === botName)) const initIntegration = async (props: TestProps, integrationName: string) => { const { tmpDir, dependencies, workspaceHandle, ...creds } = props const argv = { ...defaults, botpressHome: getHomeDir({ tmpDir }), confirm: true, ...creds, } const integrationDir = pathlib.join(tmpDir, integrationName) await impl .init({ ...argv, workDir: tmpDir, name: `${workspaceHandle}/${integrationName}`, type: 'integration', template: 'empty', }) .then(utils.handleExitCode) await utils.fixBotpressDependencies({ workDir: integrationDir, target: dependencies }) await utils.npmInstall({ workDir: integrationDir }).then(utils.handleExitCode) return { integrationDir } } export const addLocalIntegrationKeepsRelativePath: Test = { name: 'cli should store the original relative path in bpDependencies when adding a local integration', handler: async (props) => { const { tmpDir, logger, workspaceHandle, ...creds } = props const argv = { ...defaults, botpressHome: getHomeDir({ tmpDir }), confirm: true, workspaceHandle, ...creds, } const integrationName = `myintegration${utils.getUUID()}` const fullIntegrationName = `${workspaceHandle}/${integrationName}` const { integrationDir } = await initIntegration(props, integrationName) logger.info('Initializing bot') const { botDir } = await initBot( props, ['import * as sdk from "@botpress/sdk"', 'export default new sdk.BotDefinition({})'].join('\n') ) // The stored path should be relative to installPath (botDir), not process.cwd() // Use integrationDir (absolute) as packageRef to avoid cross-drive issues on Windows const rel = pathlib.relative(botDir, integrationDir).split(pathlib.sep).join('/') const expectedStoredPath = rel.startsWith('.') ? rel : `./${rel}` logger.info('Adding local integration via relative path') await impl .add({ ...argv, installPath: botDir, packageRef: integrationDir, useDev: false, alias: undefined }) .then(utils.handleExitCode) const pkgJson = JSON.parse(fslib.readFileSync(pathlib.join(botDir, 'package.json'), 'utf8')) const bpDeps = pkgJson.bpDependencies as Record | undefined if (!bpDeps) { throw new Error('Expected bpDependencies to be set in package.json after bp add') } const storedPath = bpDeps[fullIntegrationName] if (storedPath !== expectedStoredPath) { throw new Error(`Expected bpDependencies to store "${expectedStoredPath}" but got "${storedPath}"`) } }, } export const addDevIntegrationSkipsBpDependencies: Test = { name: 'cli should not modify bpDependencies when adding a local integration with --use-dev', handler: async (props) => { const { tmpDir, logger, workspaceHandle, ...creds } = props const argv = { ...defaults, botpressHome: getHomeDir({ tmpDir }), confirm: true, workspaceHandle, ...creds, } const integrationName = `myintegration${utils.getUUID()}` const fullIntegrationName = `${workspaceHandle}/${integrationName}` const { integrationDir } = await initIntegration(props, integrationName) // Simulate a dev integration by writing a devId to the project cache const cacheDir = pathlib.join(integrationDir, '.botpress') fslib.mkdirSync(cacheDir, { recursive: true }) fslib.writeFileSync( pathlib.join(cacheDir, 'project.cache.json'), JSON.stringify({ devId: 'fake-dev-integration-id' }) ) logger.info('Initializing bot') const { botDir } = await initBot( props, ['import * as sdk from "@botpress/sdk"', 'export default new sdk.BotDefinition({})'].join('\n') ) logger.info('Adding dev integration') await impl .add({ ...argv, installPath: botDir, packageRef: integrationDir, useDev: true, alias: undefined }) .then(utils.handleExitCode) const pkgJson = JSON.parse(fslib.readFileSync(pathlib.join(botDir, 'package.json'), 'utf8')) const bpDeps = pkgJson.bpDependencies as Record | undefined if (bpDeps?.[fullIntegrationName] !== undefined) { throw new Error( `Expected "${fullIntegrationName}" to NOT be in bpDependencies when using --use-dev, but got: ${JSON.stringify(bpDeps)}` ) } }, } export const addDevIntegrationPreservesExistingBpDependency: Test = { name: 'cli should not overwrite existing bpDependencies entry when re-adding as dev integration', handler: async (props) => { const { tmpDir, logger, workspaceHandle, ...creds } = props const argv = { ...defaults, botpressHome: getHomeDir({ tmpDir }), confirm: true, workspaceHandle, ...creds, } const integrationName = `myintegration${utils.getUUID()}` const fullIntegrationName = `${workspaceHandle}/${integrationName}` const { integrationDir } = await initIntegration(props, integrationName) logger.info('Initializing bot') const { botDir } = await initBot( props, ['import * as sdk from "@botpress/sdk"', 'export default new sdk.BotDefinition({})'].join('\n') ) logger.info('Adding local integration (no --use-dev) to populate bpDependencies') await impl .add({ ...argv, installPath: botDir, packageRef: integrationDir, useDev: false, alias: undefined }) .then(utils.handleExitCode) const pkgJsonAfterFirst = JSON.parse(fslib.readFileSync(pathlib.join(botDir, 'package.json'), 'utf8')) const storedRelativePath = (pkgJsonAfterFirst.bpDependencies as Record | undefined)?.[ fullIntegrationName ] if (!storedRelativePath) { throw new Error('Expected bpDependencies to contain an entry after initial bp add') } // Simulate the integration being run with bp dev (devId present in cache) const cacheDir = pathlib.join(integrationDir, '.botpress') fslib.mkdirSync(cacheDir, { recursive: true }) fslib.writeFileSync( pathlib.join(cacheDir, 'project.cache.json'), JSON.stringify({ devId: 'fake-dev-integration-id' }) ) logger.info('Re-adding same integration with --use-dev') await impl .add({ ...argv, installPath: botDir, packageRef: integrationDir, useDev: true, alias: undefined }) .then(utils.handleExitCode) const pkgJsonAfterDev = JSON.parse(fslib.readFileSync(pathlib.join(botDir, 'package.json'), 'utf8')) const bpDepsAfterDev = pkgJsonAfterDev.bpDependencies as Record | undefined if (bpDepsAfterDev?.[fullIntegrationName] !== storedRelativePath) { throw new Error( `Expected bpDependencies["${fullIntegrationName}"] to remain "${storedRelativePath}" after --use-dev add, but got: ${JSON.stringify(bpDepsAfterDev?.[fullIntegrationName])}` ) } }, } export const reinstallLocalIntegration: Test = { name: 'cli should reinstall local integration from bpDependencies regardless of cwd', handler: async (props) => { const { tmpDir, logger, workspaceHandle, ...creds } = props const argv = { ...defaults, botpressHome: getHomeDir({ tmpDir }), confirm: true, ...creds, } const integrationName = `myintegration${utils.getUUID()}` const fullIntegrationName = `${workspaceHandle}/${integrationName}` const { integrationDir } = await initIntegration(props, integrationName) logger.info('Initializing bot') const { botDir } = await initBot( props, ['import * as sdk from "@botpress/sdk"', 'export default new sdk.BotDefinition({})'].join('\n') ) logger.info('Adding local integration') await impl .add({ ...argv, installPath: botDir, packageRef: integrationDir, useDev: false, alias: undefined }) .then(utils.handleExitCode) const moduleDir = pathlib.join(botDir, consts.installDirName, cliUtils.casing.to.kebabCase(fullIntegrationName)) if (!fslib.existsSync(moduleDir)) { throw new Error(`Expected bp_modules to contain "${fullIntegrationName}" after bp add`) } logger.info('Deleting bp_modules') fslib.rmSync(pathlib.join(botDir, 'bp_modules'), { recursive: true, force: true }) logger.info('Reinstalling from bpDependencies') await impl .add({ ...argv, installPath: botDir, packageRef: undefined, useDev: false, alias: undefined }) .then(utils.handleExitCode) if (!fslib.existsSync(moduleDir)) { throw new Error(`Expected bp_modules to contain "${fullIntegrationName}" after reinstall`) } }, }