509 lines
17 KiB
TypeScript
509 lines
17 KiB
TypeScript
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<sdk.IntegrationDefinitionProps, 'name'>
|
|
|
|
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<ApiBot | undefined> =>
|
|
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<string, string> | 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<string, string> | 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<string, string> | 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<string, string> | 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`)
|
|
}
|
|
},
|
|
}
|