chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,330 @@
|
||||
import { Client } from '@botpress/client'
|
||||
import fs from 'fs/promises'
|
||||
import pathlib from 'path'
|
||||
import * as uuid from 'uuid'
|
||||
import impl from '../../src'
|
||||
import { ApiBot, ApiIntegration, ApiInterface, ApiPlugin } from '../api'
|
||||
import defaults from '../defaults'
|
||||
import * as retry from '../retry'
|
||||
import { Test } from '../typings'
|
||||
import * as utils from '../utils'
|
||||
|
||||
type ARGV = typeof defaults & {
|
||||
botpressHome: string
|
||||
confirm: true
|
||||
workspaceId: string
|
||||
workspaceHandle: string
|
||||
token: string
|
||||
apiUrl: string
|
||||
tunnelUrl: string
|
||||
sdkPath?: string
|
||||
clientPath?: string
|
||||
}
|
||||
|
||||
export const createDeployBotWithDependencies: Test = {
|
||||
name: 'cli should allow creating, building, deploying and managing a bot that has nested dependencies',
|
||||
handler: async ({ tmpDir, dependencies, logger, ...creds }) => {
|
||||
if (!_isBotpressWorkspace(creds.workspaceHandle, creds.workspaceId)) {
|
||||
// Unfortunately, only the botpress workspace can deploy interfaces
|
||||
logger.info(
|
||||
`Skipping test because the workspace is not a Botpress workspace (workspaceHandle: ${creds.workspaceHandle}, workspaceId: ${creds.workspaceId})`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const botpressHomeDir = pathlib.join(tmpDir, '.botpresshome')
|
||||
const fixturesDir = pathlib.join(process.cwd(), 'e2e', 'fixtures')
|
||||
const interfaceDir = pathlib.join(tmpDir, 'interfaces', 'interface-with-entities')
|
||||
const integrationDir = pathlib.join(tmpDir, 'integrations', 'integration-with-entity-dependency')
|
||||
const pluginDir = pathlib.join(tmpDir, 'plugins', 'plugin-with-interface-dependency')
|
||||
const botDir = pathlib.join(tmpDir, 'bots', 'bot-with-plugin-dependency')
|
||||
|
||||
// copy all subfolders from fixturesDir to tmpDir:
|
||||
await fs.cp(fixturesDir, tmpDir, { recursive: true, force: true })
|
||||
|
||||
const argv = {
|
||||
...defaults,
|
||||
botpressHome: botpressHomeDir,
|
||||
confirm: true,
|
||||
...creds,
|
||||
} satisfies ARGV
|
||||
|
||||
const client = new Client({
|
||||
apiUrl: creds.apiUrl,
|
||||
token: creds.token,
|
||||
workspaceId: creds.workspaceId,
|
||||
retry: retry.config,
|
||||
})
|
||||
await impl.login({ ...argv }).then(utils.handleExitCode)
|
||||
|
||||
// Generate names:
|
||||
const interfaceName = _generateInterfaceName()
|
||||
const integrationName = _generateIntegrationName()
|
||||
const pluginName = _generatePluginName()
|
||||
const botName = _generateBotName()
|
||||
|
||||
// Deploy dependencies:
|
||||
await using _iface = await _deployInterface({ argv, client, dependencies, workDir: interfaceDir, interfaceName })
|
||||
await using _integration = await _deployIntegration({
|
||||
argv,
|
||||
client,
|
||||
dependencies,
|
||||
workDir: integrationDir,
|
||||
integrationName,
|
||||
})
|
||||
await using _plugin = await _deployPlugin({ argv, client, dependencies, workDir: pluginDir, pluginName })
|
||||
|
||||
// Deploy bot:
|
||||
await using bot = await _deployBot({ argv, client, dependencies, workDir: botDir, botName })
|
||||
|
||||
// Fetch and verify the deployed bot:
|
||||
const deployedBot = await client.getBot({ id: bot.botId }).then((res) => res.bot)
|
||||
|
||||
// TODO: use vitest for our e2e tests and replace this series of if/else
|
||||
// checks with an expect().toMatchObject() assertion.
|
||||
|
||||
// Check that the plugin is installed and enabled:
|
||||
if (!deployedBot.plugins?.['plugin-alias']?.enabled) {
|
||||
throw new Error('Expected bot to have plugin "plugin-alias"')
|
||||
}
|
||||
|
||||
// Check that the schemas have been merged correctly:
|
||||
const action = deployedBot.actions?.['plugin-alias#doSomething']
|
||||
if (!action?.input?.schema?.properties) {
|
||||
throw new Error('Expected bot to have action "plugin-alias#doSomething"')
|
||||
}
|
||||
|
||||
const itemEntityProperties = Object.keys(action.input.schema.properties.item.properties)
|
||||
// Property defined by the interface:
|
||||
if (!itemEntityProperties.includes('name')) {
|
||||
throw new Error('Expected "plugin-alias#doSomething" action input to have item.name property')
|
||||
}
|
||||
// Property defined by the integration:
|
||||
if (!itemEntityProperties.includes('color')) {
|
||||
throw new Error('Expected "plugin-alias#doSomething" action input to have item.color property')
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const _isBotpressWorkspace = (workspaceHandle: string, workspaceId: string): boolean =>
|
||||
workspaceHandle === 'botpress' ||
|
||||
[
|
||||
'6a76fa10-e150-4ff6-8f59-a300feec06c1',
|
||||
'95de33eb-1551-4af9-9088-e5dcb02efd09',
|
||||
'11111111-1111-1111-aaaa-111111111111',
|
||||
].includes(workspaceId)
|
||||
|
||||
const _deployInterface = async ({
|
||||
argv,
|
||||
client,
|
||||
workDir,
|
||||
interfaceName,
|
||||
dependencies,
|
||||
}: {
|
||||
argv: ARGV
|
||||
client: Client
|
||||
workDir: string
|
||||
interfaceName: string
|
||||
dependencies: Record<string, string | undefined>
|
||||
}) => {
|
||||
await _editPackageJson({ workDir, interfaceName })
|
||||
await _installAndBuild({ argv, workDir, dependencies })
|
||||
|
||||
await impl
|
||||
.deploy({ ...argv, workDir, createNewBot: undefined, botId: undefined, visibility: 'public' })
|
||||
.then(utils.handleExitCode)
|
||||
|
||||
return {
|
||||
[Symbol.asyncDispose]: () => _deleteInterfaceIfExists(client, interfaceName),
|
||||
}
|
||||
}
|
||||
|
||||
const _deployIntegration = async ({
|
||||
argv,
|
||||
client,
|
||||
workDir,
|
||||
integrationName,
|
||||
dependencies,
|
||||
}: {
|
||||
argv: ARGV
|
||||
client: Client
|
||||
workDir: string
|
||||
integrationName: string
|
||||
dependencies: Record<string, string | undefined>
|
||||
}) => {
|
||||
await _editPackageJson({ workDir, integrationName })
|
||||
await _installAndBuild({ argv, workDir, dependencies })
|
||||
|
||||
await impl.deploy({ ...argv, workDir, createNewBot: undefined, botId: undefined }).then(utils.handleExitCode)
|
||||
|
||||
return {
|
||||
[Symbol.asyncDispose]: () => _deleteIntegrationIfExists(client, integrationName),
|
||||
}
|
||||
}
|
||||
|
||||
const _deployPlugin = async ({
|
||||
argv,
|
||||
client,
|
||||
workDir,
|
||||
pluginName,
|
||||
dependencies,
|
||||
}: {
|
||||
argv: ARGV
|
||||
client: Client
|
||||
workDir: string
|
||||
pluginName: string
|
||||
dependencies: Record<string, string | undefined>
|
||||
}) => {
|
||||
await _editPackageJson({ workDir, pluginName })
|
||||
await _installAndBuild({ argv, workDir, dependencies })
|
||||
await impl.deploy({ ...argv, workDir, createNewBot: undefined, botId: undefined }).then(utils.handleExitCode)
|
||||
|
||||
return {
|
||||
[Symbol.asyncDispose]: () => _deletePluginIfExists(client, pluginName),
|
||||
}
|
||||
}
|
||||
|
||||
const _deployBot = async ({
|
||||
argv,
|
||||
client,
|
||||
workDir,
|
||||
botName,
|
||||
dependencies,
|
||||
}: {
|
||||
argv: ARGV
|
||||
client: Client
|
||||
workDir: string
|
||||
botName: string
|
||||
dependencies: Record<string, string | undefined>
|
||||
}) => {
|
||||
await _editPackageJson({ workDir, botName })
|
||||
await _installAndBuild({ argv, workDir, dependencies })
|
||||
await impl.bots.create({ ...argv, name: botName, ifNotExists: false }).then(utils.handleExitCode)
|
||||
|
||||
const bot = await _fetchBotByName(client, botName)
|
||||
if (!bot) {
|
||||
throw new Error(`Bot ${botName} should have been created`)
|
||||
}
|
||||
|
||||
await impl.deploy({ ...argv, workDir, createNewBot: false, botId: bot.id }).then(utils.handleExitCode)
|
||||
|
||||
return {
|
||||
botId: bot.id,
|
||||
[Symbol.asyncDispose]: () => _deleteBotIfExists(client, botName),
|
||||
}
|
||||
}
|
||||
|
||||
const _installAndBuild = async ({
|
||||
argv,
|
||||
workDir,
|
||||
dependencies,
|
||||
}: {
|
||||
argv: ARGV
|
||||
workDir: string
|
||||
dependencies: Record<string, string | undefined>
|
||||
}) => {
|
||||
await utils.fixBotpressDependencies({ workDir, target: dependencies })
|
||||
await utils.npmInstall({ workDir }).then(utils.handleExitCode)
|
||||
|
||||
const prevDir = process.cwd()
|
||||
process.chdir(workDir)
|
||||
await impl
|
||||
.add({
|
||||
...argv,
|
||||
installPath: workDir,
|
||||
packageRef: undefined,
|
||||
useDev: false,
|
||||
alias: undefined,
|
||||
})
|
||||
.then(utils.handleExitCode)
|
||||
process.chdir(prevDir)
|
||||
|
||||
await impl.build({ ...argv, workDir }).then(utils.handleExitCode)
|
||||
}
|
||||
|
||||
const _fetchBotByName = (client: Client, botName: string): Promise<ApiBot | undefined> =>
|
||||
client.list
|
||||
.bots({})
|
||||
.collect()
|
||||
.then((bots) => bots.find(({ name }) => name === botName))
|
||||
|
||||
const _fetchIntegrationByName = (client: Client, integrationName: string): Promise<ApiIntegration | undefined> =>
|
||||
client.list
|
||||
.integrations({})
|
||||
.collect()
|
||||
.then((integrations) => integrations.find(({ name }) => name === integrationName))
|
||||
|
||||
const _fetchInterfaceByName = (client: Client, interfaceName: string): Promise<ApiInterface | undefined> =>
|
||||
client.list
|
||||
.publicInterfaces({})
|
||||
.collect()
|
||||
.then((interfaces) => interfaces.find(({ name }) => name === interfaceName))
|
||||
|
||||
const _fetchPluginByName = (client: Client, pluginName: string): Promise<ApiPlugin | undefined> =>
|
||||
client.list
|
||||
.plugins({})
|
||||
.collect()
|
||||
.then((plugins) => plugins.find(({ name }) => name === pluginName))
|
||||
|
||||
const _editPackageJson = async (payload: { workDir: string; [k: string]: string }) => {
|
||||
const { workDir, ...modifications } = payload
|
||||
const packageJsonPath = pathlib.join(workDir, 'package.json')
|
||||
const originalPackageJson: Record<string, unknown> = JSON.parse(await fs.readFile(packageJsonPath, 'utf-8'))
|
||||
|
||||
const newPackageJson = {
|
||||
...originalPackageJson,
|
||||
...modifications,
|
||||
}
|
||||
|
||||
await fs.writeFile(packageJsonPath, JSON.stringify(newPackageJson, null, 2))
|
||||
}
|
||||
|
||||
const _generateInterfaceName = () => `iface-${uuid.v4()}` as const
|
||||
const _generateIntegrationName = () => `integration-${uuid.v4()}` as const
|
||||
const _generatePluginName = () => `plugin-${uuid.v4()}` as const
|
||||
const _generateBotName = () => `bot-${uuid.v4()}` as const
|
||||
|
||||
const _deleteBotIfExists = async (client: Client, botName: string): Promise<void> => {
|
||||
console.debug(`Deleting bot: ${botName}`)
|
||||
const bot = await _fetchBotByName(client, botName)
|
||||
if (!bot) {
|
||||
return
|
||||
}
|
||||
await _swallowErrors(client.deleteBot({ id: bot.id })).then(() => utils.sleep(10_000))
|
||||
}
|
||||
|
||||
const _deleteIntegrationIfExists = async (client: Client, integrationName: string): Promise<void> => {
|
||||
console.debug(`Deleting integration: ${integrationName}`)
|
||||
const integration = await _fetchIntegrationByName(client, integrationName)
|
||||
if (!integration) {
|
||||
return
|
||||
}
|
||||
await _swallowErrors(client.deleteIntegration({ id: integration.id }))
|
||||
}
|
||||
|
||||
const _deleteInterfaceIfExists = async (client: Client, interfaceName: string): Promise<void> => {
|
||||
console.debug(`Deleting interface: ${interfaceName}`)
|
||||
const iface = await _fetchInterfaceByName(client, interfaceName)
|
||||
if (!iface) {
|
||||
return
|
||||
}
|
||||
await _swallowErrors(client.deleteInterface({ id: iface.id }))
|
||||
}
|
||||
|
||||
const _deletePluginIfExists = async (client: Client, pluginName: string): Promise<void> => {
|
||||
console.debug(`Deleting plugin: ${pluginName}`)
|
||||
const plugin = await _fetchPluginByName(client, pluginName)
|
||||
if (!plugin) {
|
||||
return
|
||||
}
|
||||
await _swallowErrors(client.deletePlugin({ id: plugin.id }))
|
||||
}
|
||||
|
||||
const _swallowErrors = async (promise: Promise<unknown>) => {
|
||||
try {
|
||||
await promise
|
||||
} catch (thrown: unknown) {
|
||||
// Swallow errors to allow cleanup to proceed:
|
||||
console.warn('Error during cleanup:', thrown)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user