Skip to content

Commit

Permalink
plugin-flow-builder: fallback , keyword, smart-intent tests (#2820)
Browse files Browse the repository at this point in the history
## Description

- Split the bot-action and first-interaction tests into two different
files.
- Add tests for fallback, keyword and smart intent. 
- Refactor the function getSmartIntentNodeByInput into a SmartIntentApi
class and add a mock for the private function getInference of this class

## Context

We need to create tests to keep adding new features and refactor the
plugin without changing the behaviour

## Approach taken / Explain the design

For the smart intent test I have created a separate flow so that I don't
always have to use the mock (we can discuss if this is the best way or
if all tests should go in the same flow)

## Testing

This PR only contains changes to add tests and refactors the
getSmartIntentNodeByInput function into a class and then creates a mock
that modifies only one function of this class.

Add tests for fallback, keyword and smart intent
  • Loading branch information
Iru89 committed May 14, 2024
1 parent 5930a1d commit 0a04c04
Show file tree
Hide file tree
Showing 12 changed files with 989 additions and 144 deletions.
12 changes: 4 additions & 8 deletions packages/botonic-plugin-flow-builder/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,23 +98,19 @@ export default class BotonicPluginFlowBuilder implements Plugin {

private updateRequestBeforeRoutes(request: PluginPreRequest) {
if (request.input.payload) {
request.input.payload = this.removeSourceSeparatorFromPayload(
request.input.payload
)
request.input.payload = this.removeSourceSufix(request.input.payload)

if (request.input.payload.startsWith(BOT_ACTION_PAYLOAD_PREFIX)) {
request.input.payload = this.updateBotActionPayload(
request.input.payload
)
request.input.payload = this.replacePayload(request.input.payload)
}
}
}

private removeSourceSeparatorFromPayload(payload: string): string {
private removeSourceSufix(payload: string): string {
return payload.split(SOURCE_INFO_SEPARATOR)[0]
}

private updateBotActionPayload(payload: string): string {
private replacePayload(payload: string): string {
const botActionId = payload.split(SEPARATOR)[1]
const botActionNode = this.cmsApi.getNodeById<HtBotActionNode>(botActionId)
return this.cmsApi.createPayloadWithParams(botActionNode)
Expand Down
5 changes: 3 additions & 2 deletions packages/botonic-plugin-flow-builder/src/user-input/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
} from '../content-fields/hubtype-fields'
import { getIntentNodeByInput } from './intent'
import { getKeywordNodeByInput } from './keyword'
import { getSmartIntentNodeByInput } from './smart-intent'
import { SmartIntentsApi } from './smart-intent'

export async function getNodeByUserInput(
cmsApi: FlowBuilderApi,
Expand All @@ -24,7 +24,8 @@ export async function getNodeByUserInput(
)
if (keywordNode) return keywordNode

const smartIntentNode = await getSmartIntentNodeByInput(cmsApi, request)
const smartIntentsApi = new SmartIntentsApi(cmsApi, request)
const smartIntentNode = smartIntentsApi.getNodeByInput()
if (smartIntentNode) return smartIntentNode

const intentNode = await getIntentNodeByInput(cmsApi, locale, request)
Expand Down
80 changes: 52 additions & 28 deletions packages/botonic-plugin-flow-builder/src/user-input/smart-intent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,43 +4,67 @@ import axios from 'axios'
import { FlowBuilderApi } from '../api'
import { HtSmartIntentNode } from '../content-fields/hubtype-fields/smart-intent'

export async function getSmartIntentNodeByInput(
cmsApi: FlowBuilderApi,
request: ActionRequest
): Promise<HtSmartIntentNode | undefined> {
const smartIntentNodes = cmsApi.getSmartIntentNodes()
if (smartIntentNodes.length === 0) {
return undefined
interface InferenceParam {
name: string
definition: string
}
export class SmartIntentsApi {
public cmsApi: FlowBuilderApi
public currentRequest: ActionRequest

constructor(cmsApi: FlowBuilderApi, request: ActionRequest) {
this.currentRequest = request
this.cmsApi = cmsApi
}

const intentsInferenceParams = smartIntentNodes.map(smartIntentNode => {
return {
name: smartIntentNode.content.title,
definition: smartIntentNode.content.description,
async getNodeByInput(): Promise<HtSmartIntentNode | undefined> {
const smartIntentNodes = this.cmsApi.getSmartIntentNodes()

if (smartIntentNodes.length === 0) {
return undefined
}
})
intentsInferenceParams.push({
name: 'Other',
definition: 'The text does not belong to any other intent.',
})

try {
const response = await axios({

const params = this.getInferenceParams(smartIntentNodes)
try {
const response = await this.getInference(params)
return smartIntentNodes.find(
smartIntentNode =>
smartIntentNode.content.title === response.data.intent_name
)
} catch (e) {
console.error(e)
return undefined
}
}

private getInferenceParams(
smartIntentNodes: HtSmartIntentNode[]
): InferenceParam[] {
const intentsInferenceParams = smartIntentNodes.map(smartIntentNode => {
return {
name: smartIntentNode.content.title,
definition: smartIntentNode.content.description,
}
})
intentsInferenceParams.push({
name: 'Other',
definition: 'The text does not belong to any other intent.',
})
return intentsInferenceParams
}

private async getInference(
inferenceParams: InferenceParam[]
): Promise<{ data: { intent_name: string } }> {
return await axios({
method: 'POST',
url: `${process.env.HUBTYPE_API_URL}/external/v1/ai/smart_intents/inference/`,
headers: {
Authorization: `Bearer ${request.session._access_token}`,
Authorization: `Bearer ${this.currentRequest.session._access_token}`,
'Content-Type': 'application/json',
},
data: { text: request.input.data, intents: intentsInferenceParams },
data: { text: this.currentRequest.input.data, intents: inferenceParams },
timeout: 10000,
})
return smartIntentNodes.find(
smartIntentNode =>
smartIntentNode.content.title === response.data.intent_name
)
} catch (e) {
console.error(e)
return undefined
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { jest } from '@jest/globals'

import { SmartIntentsApi } from '../../src/user-input/smart-intent'

export function mockSmartIntent(intentName?: string) {
// Spy on the private function getInference
const getInferenceSpy = jest.spyOn(
SmartIntentsApi.prototype as any,
'getInference'
)

// Change the implementation of getInference
getInferenceSpy.mockImplementation(async () => {
return intentName ? { data: { intent_name: intentName } } : undefined
})
}
46 changes: 18 additions & 28 deletions packages/botonic-plugin-flow-builder/tests/bot-action.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,24 @@ import { describe, test } from '@jest/globals'
import { BOT_ACTION_PAYLOAD_PREFIX } from '../src/constants'
import { FlowText } from '../src/index'
import { ProcessEnvNodeEnvs } from '../src/types'
import { testFlow } from './helpers/flows'
import { basicFlow } from './helpers/flows/basic'
import {
createFlowBuilderPlugin,
createRequest,
getContentsAfterPreAndBotonicInit,
} from './helpers/utils'

describe('Check the contents returned by the plugin', () => {
describe('The user clicks on a button that is connected to a BotActionNode', () => {
process.env.NODE_ENV = ProcessEnvNodeEnvs.PRODUCTION
const flowBuilderPlugin = createFlowBuilderPlugin(testFlow)

test('The starting content is displayed on the first interaction', async () => {
const flowBuilderPlugin = createFlowBuilderPlugin(basicFlow)
const ratingMessageUuid = '578b30eb-d230-4162-8a36-6c7fa18ff0db'
const botActionUuid = '85dbeb56-81c9-419d-a235-4ebf491b4fc9'
test('The button has a payload equal to ba|botActionUuid', async () => {
const request = createRequest({
input: { data: 'Hola', type: INPUT.TEXT },
isFirstInteraction: true,
input: {
type: INPUT.POSTBACK,
payload: ratingMessageUuid,
},
plugins: {
// @ts-ignore
flowBuilderPlugin,
Expand All @@ -30,38 +33,29 @@ describe('Check the contents returned by the plugin', () => {
flowBuilderPlugin
)

expect((contents[0] as FlowText).text).toBe('Welcome message')
const nextPaylod = (contents[0] as FlowText).buttons[0].payload
expect(nextPaylod).toBe(`${BOT_ACTION_PAYLOAD_PREFIX}${botActionUuid}`)
})
})

describe('The user clicks on a button that is connected to a BotActionNode', () => {
process.env.NODE_ENV = ProcessEnvNodeEnvs.PRODUCTION
const flowBuilderPlugin = createFlowBuilderPlugin(testFlow)
const messageUUidWithButtonConectedToBotAction =
'386ba508-a3b3-49a2-94d0-5e239ba63106'
const botActionUuid = '8b0c87c0-77b2-4b05-bae0-3b353240caaa'
test('The button has the payload = ba|botActionUuid', async () => {
test('The bot routes receive the correct payload', async () => {
const request = createRequest({
input: {
type: INPUT.POSTBACK,
payload: messageUUidWithButtonConectedToBotAction,
payload: `${BOT_ACTION_PAYLOAD_PREFIX}${botActionUuid}`,
},
plugins: {
// @ts-ignore
flowBuilderPlugin,
},
})

const { contents } = await getContentsAfterPreAndBotonicInit(
request,
flowBuilderPlugin
await flowBuilderPlugin.pre(request)
expect(request.input.payload).toBe(
'rating|{"value":1,"followUpContentID":"SORRY"}'
)

const nextPaylod = (contents[0] as FlowText).buttons[0].payload
expect(nextPaylod).toBe(`${BOT_ACTION_PAYLOAD_PREFIX}${botActionUuid}`)
})

test('The bot routes receive the correct payload, in the custom action the payloadParmas defined in the BotActionNode are obtained', async () => {
test('In the custom action the payloadParmas defined in the BotActionNode are obtained', async () => {
const request = createRequest({
input: {
type: INPUT.POSTBACK,
Expand All @@ -74,10 +68,6 @@ describe('The user clicks on a button that is connected to a BotActionNode', ()
})

await flowBuilderPlugin.pre(request)
expect(request.input.payload).toBe(
'rating|{"value":1,"followUpContentID":"SORRY"}'
)

const payloadParams = flowBuilderPlugin.getPayloadParams(
request.input.payload as string
)
Expand Down
51 changes: 51 additions & 0 deletions packages/botonic-plugin-flow-builder/tests/fallback.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { INPUT } from '@botonic/core'
import { describe, test } from '@jest/globals'

import { FlowText } from '../src/index'
import { ProcessEnvNodeEnvs } from '../src/types'
import { basicFlow } from './helpers/flows/basic'
import {
createFlowBuilderPlugin,
createRequest,
getContentsAfterPreAndBotonicInit,
} from './helpers/utils'

describe('Check the content returned by the plugin when there is no match with payload or keyword or intents', () => {
process.env.NODE_ENV = ProcessEnvNodeEnvs.PRODUCTION
const flowBuilderPlugin = createFlowBuilderPlugin(basicFlow)

test('The content displayed changes between the 1st and 2nd fallback', async () => {
const request = createRequest({
input: { data: 'I want to cancel my flight', type: INPUT.TEXT },
plugins: {
// @ts-ignore
flowBuilderPlugin,
},
})

const firstFallback = await getContentsAfterPreAndBotonicInit(
request,
flowBuilderPlugin
)
expect((firstFallback.contents[0] as FlowText).text).toBe(
'fallback 1st message'
)

const secondFallback = await getContentsAfterPreAndBotonicInit(
request,
flowBuilderPlugin
)
expect((secondFallback.contents[0] as FlowText).text).toBe(
'fallback 2nd message'
)

const thirdFallback = await getContentsAfterPreAndBotonicInit(
request,
flowBuilderPlugin
)

expect((thirdFallback.contents[0] as FlowText).text).toBe(
'fallback 1st message'
)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { INPUT } from '@botonic/core'
import { describe, test } from '@jest/globals'

import { FlowText } from '../src/index'
import { ProcessEnvNodeEnvs } from '../src/types'
import { basicFlow } from './helpers/flows/basic'
import {
createFlowBuilderPlugin,
createRequest,
getContentsAfterPreAndBotonicInit,
} from './helpers/utils'

describe('Check the contents returned by the plugin', () => {
process.env.NODE_ENV = ProcessEnvNodeEnvs.PRODUCTION
const flowBuilderPlugin = createFlowBuilderPlugin(basicFlow)

test('The starting content is displayed on the first interaction', async () => {
const request = createRequest({
input: { data: 'Hola', type: INPUT.TEXT },
isFirstInteraction: true,
plugins: {
// @ts-ignore
flowBuilderPlugin,
},
})

const { contents } = await getContentsAfterPreAndBotonicInit(
request,
flowBuilderPlugin
)

expect((contents[0] as FlowText).text).toBe('Welcome message')
})
})

0 comments on commit 0a04c04

Please sign in to comment.