Skip to content

Commit

Permalink
fix(testing): fix testing module not working (#11687)
Browse files Browse the repository at this point in the history
  • Loading branch information
laurentlp committed Apr 1, 2022
1 parent 12f1a97 commit 5804000
Show file tree
Hide file tree
Showing 12 changed files with 123 additions and 89 deletions.
19 changes: 12 additions & 7 deletions modules/testing/src/backend/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,15 @@ export default async (bp: typeof sdk, testByBot: TestByBot) => {
res.sendStatus(200)
})

router.get('/startRecording/:userId?', async (req, res) => {
await testByBot[req.params.botId].startRecording(req.params.userId || '')
router.post('/startRecording', async (req, res) => {
if (!req.body.userId) {
return res.sendStatus(400)
}
await testByBot[req.params.botId].startRecording(req.body.userId)
res.sendStatus(200)
})

router.get('/stopRecording', async (req, res) => {
router.post('/stopRecording', async (req, res) => {
res.send(testByBot[req.params.botId].endRecording())
})

Expand All @@ -63,14 +66,16 @@ export default async (bp: typeof sdk, testByBot: TestByBot) => {
return res.sendStatus(200)
})

router.post('/incomingEvent', (req, res) => {
router.post('/incomingEvent', async (req, res) => {
const event = req.body as sdk.IO.IncomingEvent
res.send(testByBot[req.params.botId].processIncomingEvent(event))
const data = await testByBot[req.params.botId].processIncomingEvent(event)
res.send(data)
})

router.post('/processedEvent', (req, res) => {
router.post('/processedEvent', async (req, res) => {
const event = req.body as sdk.IO.IncomingEvent
res.send(testByBot[req.params.botId].processCompletedEvent(event))
const data = await testByBot[req.params.botId].processCompletedEvent(event)
res.send(data)
})

router.post('/fetchPreviews', async (req, res) => {
Expand Down
1 change: 0 additions & 1 deletion modules/testing/src/backend/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import * as sdk from 'botpress/sdk'
import _ from 'lodash'

import en from '../translations/en.json'
import fr from '../translations/fr.json'
Expand Down
24 changes: 13 additions & 11 deletions modules/testing/src/backend/recorder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,33 @@ import * as sdk from 'botpress/sdk'
import _ from 'lodash'

import { Scenario } from './typings'
import { convertLastMessages } from './utils'
import { convertLastMessages, getMappingFromVisitor } from './utils'

export class Recorder {
private _lastEvent: sdk.IO.IncomingEvent
private _lastEvent?: sdk.IO.IncomingEvent
private _scenario?: Scenario
private _specificTarget?: string
private _target!: string

constructor() {}
constructor(private bp: typeof sdk) {}

processIncoming(event: sdk.IO.IncomingEvent) {
if (!this._scenario || this._scenario.initialState) {
async processIncoming(event: sdk.IO.IncomingEvent) {
if (!this.isRecording() || this._scenario.initialState) {
return
}

if (!this._specificTarget || this._specificTarget === event.target) {
const target = await getMappingFromVisitor(this.bp, event.botId, this._target)
if (target === event.target) {
this._scenario.initialState = event.state
}
}

processCompleted(event: sdk.IO.IncomingEvent) {
if (!this._scenario) {
async processCompleted(event: sdk.IO.IncomingEvent) {
if (!this.isRecording()) {
return
}

if (this._specificTarget && this._specificTarget !== event.target) {
const target = await getMappingFromVisitor(this.bp, event.botId, this._target)
if (target !== event.target) {
return
}

Expand All @@ -45,7 +47,7 @@ export class Recorder {
steps: []
}

this._specificTarget = chatUserId.length > 0 && chatUserId
this._target = chatUserId
}

stopRecording(): Partial<Scenario> | void {
Expand Down
26 changes: 12 additions & 14 deletions modules/testing/src/backend/runner.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import * as sdk from 'botpress/sdk'
import _ from 'lodash'

import { DialogStep, RunningScenario, Scenario, ScenarioMismatch, ScenarioStatus } from './typings'
import { DialogStep, RunningScenario, Scenario, ScenarioMismatch, ScenarioStatus, Status } from './typings'
import { convertLastMessages } from './utils'

const TIMEOUT = 3000

export class SenarioRunner {
export class ScenarioRunner {
private _active: RunningScenario[]
private _status: ScenarioStatus
private _interval: any
private _interval: NodeJS.Timeout

constructor(private bp: typeof sdk) {
this._active = []
Expand Down Expand Up @@ -143,20 +143,18 @@ export class SenarioRunner {
this._active = this._active.filter(x => x.name !== name)
}

private _updateStatus(scenario, obj) {
private _updateStatus(scenario: string, obj: Partial<Status>) {
this._status[scenario] = { ...(this._status[scenario] || {}), ...obj }
}

private _sendMessage = (message: string, eventDestination: sdk.IO.EventDestination) => {
setTimeout(async () => {
const event = this.bp.IO.Event({
...eventDestination,
direction: 'incoming',
payload: { type: 'text', text: message },
type: 'text'
})

await this.bp.events.sendEvent(event)
}, 1000)
const event = this.bp.IO.Event({
...eventDestination,
direction: 'incoming',
payload: { type: 'text', text: message },
type: 'text'
})

this.bp.events.sendEvent(event)
}
}
72 changes: 33 additions & 39 deletions modules/testing/src/backend/testing.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,26 @@
import * as sdk from 'botpress/sdk'
import _ from 'lodash'
import { nanoid } from 'nanoid'
import path from 'path'

import { Recorder } from './recorder'
import { SenarioRunner } from './runner'
import { ScenarioRunner } from './runner'
import { Scenario } from './typings'
import { buildScenarioFromEvents } from './utils'

const SCENARIO_FOLDER = 'scenarios'

export class Testing {
private bp: typeof sdk
private botId: string
private _recorder: Recorder
private _runner: SenarioRunner
private _runner: ScenarioRunner
private _scenarios: Scenario[]
private _interval: any
private _interval?: NodeJS.Timeout

constructor(bp: typeof sdk, botId: string) {
this.bp = bp
this.botId = botId
this._recorder = new Recorder()
this._runner = new SenarioRunner(bp)
constructor(private bp: typeof sdk, private _botId: string) {
this._recorder = new Recorder(bp)
this._runner = new ScenarioRunner(bp)
}

async startRecording(chatUserId) {
async startRecording(chatUserId: string) {
await this._ensureHooksEnabled()
this._recorder.startRecording(chatUserId)
}
Expand Down Expand Up @@ -55,13 +50,13 @@ export class Testing {
})
}

processIncomingEvent(event: sdk.IO.IncomingEvent): sdk.IO.EventState | void {
this._recorder.processIncoming(event)
return this._runner.processIncoming(event)
async processIncomingEvent(event: sdk.IO.IncomingEvent): Promise<sdk.IO.EventState | void> {
await this._recorder.processIncoming(event)
this._runner.processIncoming(event)
}

processCompletedEvent(event: sdk.IO.IncomingEvent): void {
this._recorder.processCompleted(event)
async processCompletedEvent(event: sdk.IO.IncomingEvent): Promise<void> {
await this._recorder.processCompleted(event)
this._runner.processCompleted(event)
}

Expand All @@ -77,22 +72,22 @@ export class Testing {
return buildScenarioFromEvents(events)
}

async saveScenario(name, scenario) {
async saveScenario(name: string, scenario: Scenario) {
await this.bp.ghost
.forBot(this.botId)
.forBot(this._botId)
.upsertFile(SCENARIO_FOLDER, `${name}.json`, JSON.stringify(scenario, undefined, 2))

await this._loadScenarios()
}

async deleteScenario(name) {
const exists = await this.bp.ghost.forBot(this.botId).fileExists(SCENARIO_FOLDER, `${name}.json`)
async deleteScenario(name: string) {
const exists = await this.bp.ghost.forBot(this._botId).fileExists(SCENARIO_FOLDER, `${name}.json`)

if (!exists) {
return
}

await this.bp.ghost.forBot(this.botId).deleteFile(SCENARIO_FOLDER, `${name}.json`)
await this.bp.ghost.forBot(this._botId).deleteFile(SCENARIO_FOLDER, `${name}.json`)
await this._loadScenarios()
}

Expand All @@ -106,37 +101,26 @@ export class Testing {
)
}

private _executeScenario(scenario: Scenario) {
const eventDestination: sdk.IO.EventDestination = {
channel: 'web',
botId: this.botId,
threadId: undefined,
target: `test_${nanoid()}`
}

this._runner.runScenario({ ...scenario }, eventDestination)
}

async executeSingle(liteScenario: Partial<Scenario>) {
await this._ensureHooksEnabled()
this._runner.startReplay()

// TODO perform scenario validation here
const scenario: Scenario = await this.bp.ghost
.forBot(this.botId)
.forBot(this._botId)
.readFileAsObject(SCENARIO_FOLDER, `${liteScenario.name}.json`)

this._executeScenario({ ...liteScenario, ...scenario })
return this._executeScenario({ ...liteScenario, ...scenario })
}

async executeAll() {
await this._ensureHooksEnabled()
const scenarios = await this._loadScenarios()
this._runner.startReplay()

scenarios.forEach(scenario => {
for (const scenario of scenarios) {
this._executeScenario(scenario)
})
}
}

private async _ensureHooksEnabled() {
Expand All @@ -158,13 +142,23 @@ export class Testing {
}
}

private _executeScenario(scenario: Scenario) {
const eventDestination: sdk.IO.EventDestination = {
channel: 'testing',
botId: this._botId,
target: `test_${nanoid()}`
}

this._runner.runScenario({ ...scenario }, eventDestination)
}

private async _loadScenarios() {
const files = await this.bp.ghost.forBot(this.botId).directoryListing(SCENARIO_FOLDER, '*.json')
const files = await this.bp.ghost.forBot(this._botId).directoryListing(SCENARIO_FOLDER, '*.json')

this._scenarios = await Promise.map(files, async file => {
const name = path.basename(file as string, '.json')
const scenarioSteps = (await this.bp.ghost
.forBot(this.botId)
.forBot(this._botId)
.readFileAsObject(SCENARIO_FOLDER, file)) as Scenario[]

return { name, ...scenarioSteps }
Expand Down
11 changes: 7 additions & 4 deletions modules/testing/src/backend/typings.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,14 @@ export type RunningScenario = {
completedSteps?: DialogStep[]
} & Scenario

export interface Status {
status?: 'pass' | 'fail' | 'pending'
mismatch?: ScenarioMismatch
completedSteps?: number
}

export interface ScenarioStatus {
[scenarioName: string]: {
status?: 'pass' | 'fail' | 'pending'
mismatch?: ScenarioMismatch
}
[scenarioName: string]: Status
}

export interface ScenarioMismatch {
Expand Down
30 changes: 25 additions & 5 deletions modules/testing/src/backend/utils.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import * as sdk from 'botpress/sdk'
import _ from 'lodash'

export const convertLastMessages = (lastMessages, eventId) => {
export const convertLastMessages = (lastMessages: sdk.IO.DialogTurnHistory[], eventId: string) => {
if (!lastMessages) {
return
}
const lastConvo = eventId ? lastMessages.filter(x => x.eventId === eventId) : lastMessages
const lastConversation = eventId ? lastMessages.filter(x => x.eventId === eventId) : lastMessages

if (!lastConvo.length) {
if (!lastConversation.length) {
return
}

return {
userMessage: lastConvo[0].incomingPreview,
botReplies: lastConvo.map(x => {
userMessage: lastConversation[0].incomingPreview,
botReplies: lastConversation.map(x => {
return {
botResponse: x.replyPreview === undefined ? null : x.replyPreview,
replySource: x.replySource
Expand Down Expand Up @@ -48,3 +48,23 @@ export const buildScenarioFromEvents = (storedEvents: sdk.IO.StoredEvent[]) => {
'finalState.context.queue'
])
}

export const getMappingFromVisitor = async (
bp: typeof sdk,
botId: string,
visitorId: string
): Promise<string | undefined> => {
try {
const rows = await bp.database('web_user_map').where({ botId, visitorId })

if (rows?.length) {
const mapping = rows[0]

return mapping.userId
}
} catch (err) {
bp.logger.error('An error occurred while fetching a visitor mapping.', err)

return undefined
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ async function execute() {
const axiosConfig = await bp.http.getAxiosConfigForBot(event.botId, { localUrl: true })
await axios.post('/mod/testing/processedEvent', event, axiosConfig)
} catch (err) {
console.log('Error processing', err.message)
console.error('Error processing', err.message)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ async function processIncoming() {
event.state = _.merge(event.state, data)
}
} catch (err) {
console.log('Error processing', err.message)
console.error('Error processing', err.message)
}
}

Expand Down

0 comments on commit 5804000

Please sign in to comment.