botium-core 1.13.16 → 1.13.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/README.md +0 -1
  2. package/dist/botium-cjs.js +283 -172
  3. package/dist/botium-cjs.js.map +1 -1
  4. package/dist/botium-es.js +282 -171
  5. package/dist/botium-es.js.map +1 -1
  6. package/package.json +23 -23
  7. package/src/BotDriver.js +2 -4
  8. package/src/Capabilities.js +7 -3
  9. package/src/Events.js +1 -0
  10. package/src/containers/BaseContainer.js +5 -3
  11. package/src/containers/PluginConnectorContainer.js +0 -4
  12. package/src/containers/plugins/SimpleRestContainer.js +49 -0
  13. package/src/scripting/Convo.js +10 -27
  14. package/src/scripting/ScriptingProvider.js +121 -53
  15. package/src/scripting/helper.js +9 -2
  16. package/src/scripting/logichook/asserter/ButtonsAsserter.js +21 -8
  17. package/src/scripting/logichook/asserter/WerAsserter.js +53 -6
  18. package/src/scripting/logichook/logichooks/ClearQueueLogicHook.js +0 -1
  19. package/test/connectors/logicHook.js +0 -1
  20. package/test/connectors/simplerest.spec.js +79 -4
  21. package/test/scripting/asserters/buttonsAsserter.spec.js +84 -50
  22. package/test/scripting/logichooks/convos/custom_embedded_skip.convo.txt +11 -0
  23. package/test/scripting/logichooks/convos/custom_embedded_skip_followed_by_me.convo.txt +11 -0
  24. package/test/scripting/logichooks/convos/custom_embedded_skip_followed_by_nothing.convo.txt +8 -0
  25. package/test/scripting/logichooks/customEmbeddedSkip.json +14 -0
  26. package/test/scripting/logichooks/customEmbeddedSkip.spec.js +58 -0
  27. package/test/security/allowUnsafe.spec.js +20 -10
  28. package/test/security/convos/withscriptingmemoryfunction.convo.txt +1 -0
  29. package/test/convo/retryconvo.spec.js +0 -134
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "botium-core",
3
- "version": "1.13.16",
3
+ "version": "1.13.18",
4
4
  "description": "The Selenium for Chatbots",
5
5
  "main": "index.js",
6
6
  "module": "dist/botium-es.js",
@@ -32,66 +32,66 @@
32
32
  },
33
33
  "homepage": "https://www.botium.ai",
34
34
  "dependencies": {
35
- "@babel/runtime": "^7.20.6",
35
+ "@babel/runtime": "^7.21.5",
36
36
  "async": "^3.2.4",
37
- "body-parser": "^1.20.1",
37
+ "body-parser": "^1.20.2",
38
38
  "boolean": "^3.2.0",
39
39
  "bottleneck": "^2.19.5",
40
- "csv-parse": "^5.3.3",
40
+ "csv-parse": "^5.3.10",
41
41
  "debug": "^4.3.4",
42
42
  "esprima": "^4.0.1",
43
43
  "express": "^4.18.2",
44
44
  "globby": "11.0.4",
45
- "ioredis": "^5.2.4",
45
+ "ioredis": "^5.3.2",
46
46
  "is-class": "^0.0.9",
47
47
  "is-json": "^2.0.1",
48
48
  "jsonpath": "^1.1.1",
49
49
  "lodash": "^4.17.21",
50
50
  "markdown-it": "^13.0.1",
51
51
  "mime-types": "^2.1.35",
52
- "mkdirp": "^1.0.4",
52
+ "mkdirp": "^3.0.1",
53
53
  "moment": "^2.29.4",
54
54
  "mustache": "^4.2.0",
55
55
  "promise-retry": "^2.0.1",
56
56
  "promise.allsettled": "^1.0.6",
57
57
  "randomatic": "^3.1.1",
58
58
  "request": "^2.88.2",
59
- "rimraf": "^3.0.2",
59
+ "rimraf": "^5.0.0",
60
60
  "sanitize-filename": "^1.6.3",
61
- "slugify": "^1.6.5",
62
- "socket.io": "^4.5.4",
63
- "socket.io-client": "^4.5.4",
61
+ "slugify": "^1.6.6",
62
+ "socket.io": "^4.6.1",
63
+ "socket.io-client": "^4.6.1",
64
64
  "socketio-auth": "^0.1.1",
65
- "swagger-jsdoc": "^6.2.5",
66
- "swagger-ui-express": "^4.6.0",
65
+ "swagger-jsdoc": "^6.2.8",
66
+ "swagger-ui-express": "^4.6.3",
67
67
  "uuid": "^9.0.0",
68
- "vm2": "^3.9.13",
68
+ "vm2": "^3.9.17",
69
69
  "word-error-rate": "0.0.7",
70
70
  "write-yaml": "^1.0.0",
71
71
  "xlsx": "^0.18.5",
72
72
  "xregexp": "^5.1.1",
73
- "yaml": "^2.1.3"
73
+ "yaml": "^2.2.2"
74
74
  },
75
75
  "devDependencies": {
76
- "@babel/core": "^7.20.5",
77
- "@babel/node": "^7.20.5",
78
- "@babel/plugin-transform-runtime": "^7.19.6",
79
- "@babel/preset-env": "^7.20.2",
76
+ "@babel/core": "^7.21.8",
77
+ "@babel/node": "^7.20.7",
78
+ "@babel/plugin-transform-runtime": "^7.21.4",
79
+ "@babel/preset-env": "^7.21.5",
80
80
  "chai": "^4.3.7",
81
81
  "chai-as-promised": "^7.1.1",
82
82
  "cross-env": "^7.0.3",
83
- "eslint": "^8.29.0",
83
+ "eslint": "^8.40.0",
84
84
  "eslint-config-standard": "^17.0.0",
85
- "eslint-plugin-import": "^2.26.0",
85
+ "eslint-plugin-import": "^2.27.5",
86
86
  "eslint-plugin-mocha": "^10.1.0",
87
- "eslint-plugin-n": "^15.6.0",
87
+ "eslint-plugin-n": "^15.7.0",
88
88
  "eslint-plugin-promise": "^6.1.1",
89
89
  "eslint-plugin-standard": "^4.1.0",
90
90
  "license-checker": "^25.0.1",
91
91
  "license-compatibility-checker": "^0.3.5",
92
92
  "mocha": "^10.2.0",
93
- "nock": "^13.2.9",
94
- "npm-check-updates": "^16.5.6",
93
+ "nock": "^13.3.1",
94
+ "npm-check-updates": "^16.10.12",
95
95
  "nyc": "^15.1.0",
96
96
  "rollup": "2.79.1",
97
97
  "rollup-plugin-babel": "^4.4.0",
package/src/BotDriver.js CHANGED
@@ -2,7 +2,7 @@ const util = require('util')
2
2
  const fs = require('fs')
3
3
  const path = require('path')
4
4
  const async = require('async')
5
- const rimraf = require('rimraf')
5
+ const { rimraf } = require('rimraf')
6
6
  const mkdirp = require('mkdirp')
7
7
  const sanitize = require('sanitize-filename')
8
8
  const moment = require('moment')
@@ -168,9 +168,7 @@ module.exports = class BotDriver {
168
168
  debug(`BotDriver Build error: ${err}`)
169
169
  this.eventEmitter.emit(Events.CONTAINER_BUILD_ERROR, err)
170
170
  if (tempDirectory) {
171
- rimraf(tempDirectory, (err) => {
172
- if (err) debug(`Cleanup temp dir ${tempDirectory} failed: ${util.inspect(err)}`)
173
- })
171
+ rimraf(tempDirectory).catch((err) => debug(`Cleanup temp dir ${tempDirectory} failed: ${util.inspect(err)}`))
174
172
  }
175
173
  return reject(err)
176
174
  }
@@ -68,6 +68,12 @@ module.exports = {
68
68
  SIMPLEREST_POLL_INTERVAL: 'SIMPLEREST_POLL_INTERVAL',
69
69
  SIMPLEREST_POLL_TIMEOUT: 'SIMPLEREST_PING_TIMEOUT',
70
70
  SIMPLEREST_POLL_UPDATE_CONTEXT: 'SIMPLEREST_POLL_UPDATE_CONTEXT',
71
+ SIMPLEREST_CONTEXT_IGNORE_JSONPATH: 'SIMPLEREST_CONTEXT_IGNORE_JSONPATH',
72
+ SIMPLEREST_CONTEXT_IGNORE_MATCH: 'SIMPLEREST_CONTEXT_IGNORE_MATCH',
73
+ SIMPLEREST_CONTEXT_SKIP_JSONPATH: 'SIMPLEREST_CONTEXT_SKIP_JSONPATH',
74
+ SIMPLEREST_CONTEXT_SKIP_MATCH: 'SIMPLEREST_CONTEXT_SKIP_MATCH',
75
+ SIMPLEREST_CONTEXT_CONTINUE_JSONPATH: 'SIMPLEREST_CONTEXT_CONTINUE_JSONPATH',
76
+ SIMPLEREST_CONTEXT_CONTINUE_MATCH: 'SIMPLEREST_CONTEXT_CONTINUE_MATCH',
71
77
  SIMPLEREST_BODY_JSONPATH: 'SIMPLEREST_BODY_JSONPATH',
72
78
  SIMPLEREST_RESPONSE_JSONPATH: 'SIMPLEREST_RESPONSE_JSONPATH',
73
79
  SIMPLEREST_RESPONSE_HOOK: 'SIMPLEREST_RESPONSE_HOOK',
@@ -165,7 +171,5 @@ module.exports = {
165
171
  RATELIMIT_USERSAYS_MINTIME: 'RATELIMIT_USERSAYS_MINTIME',
166
172
  RATELIMIT_BOTTLENECK_FN: 'RATELIMIT_BOTTLENECK_FN',
167
173
  SECURITY_ALLOW_UNSAFE: 'SECURITY_ALLOW_UNSAFE',
168
- PRECOMPILERS: 'PRECOMPILERS',
169
- // RETRY
170
- RETRY_CONVO_ASYNC: 'RETRY_CONVO_ASYNC'
174
+ PRECOMPILERS: 'PRECOMPILERS'
171
175
  }
package/src/Events.js CHANGED
@@ -13,6 +13,7 @@ module.exports = {
13
13
  CONTAINER_CLEANED: 'CONTAINER_CLEANED',
14
14
  CONTAINER_CLEAN_ERROR: 'CONTAINER_CLEAN_ERROR',
15
15
  BOT_CONNECTED: 'BOT_CONNECTED',
16
+ CONVO_STEP_NEXT: 'CONVO_STEP_NEXT',
16
17
  // Chatbot Events
17
18
  MESSAGE_SENTTOBOT: 'MESSAGE_SENTTOBOT',
18
19
  MESSAGE_SENDTOBOT_ERROR: 'MESSAGE_SENDTOBOT_ERROR',
@@ -173,10 +173,12 @@ module.exports = class BaseContainer {
173
173
  (rimraffed) => {
174
174
  if (this.caps[Capabilities.CLEANUPTEMPDIR]) {
175
175
  debug(`Cleanup rimrafing temp dir ${this.tempDirectory}`)
176
- rimraf(this.tempDirectory, (err) => {
177
- if (err) debug(`Cleanup temp dir ${this.tempDirectory} failed: ${util.inspect(err)}`)
176
+ try {
177
+ rimraf.sync(this.tempDirectory)
178
178
  rimraffed()
179
- })
179
+ } catch (err) {
180
+ rimraffed(new Error(`Cleanup temp directory ${this.tempDirectory} failed: ${util.inspect(err)}`))
181
+ }
180
182
  } else {
181
183
  rimraffed()
182
184
  }
@@ -11,16 +11,12 @@ const RetryHelper = require('../helpers/RetryHelper')
11
11
  module.exports = class PluginConnectorContainer extends BaseContainer {
12
12
  async Validate () {
13
13
  await super.Validate()
14
- const setAsync = (isAsync) => {
15
- this.caps.RETRY_CONVO_ASYNC = isAsync
16
- }
17
14
  this.pluginInstance = tryLoadPlugin(
18
15
  this.caps[Capabilities.CONTAINERMODE],
19
16
  this.caps[Capabilities.PLUGINMODULEPATH],
20
17
  {
21
18
  container: this,
22
19
  queueBotSays: (msg) => this._QueueBotSays(msg),
23
- setAsync: (isAsync) => setAsync(isAsync),
24
20
  bottleneck: this.bottleneck,
25
21
  eventEmitter: this.eventEmitter,
26
22
  caps: this.caps,
@@ -293,6 +293,47 @@ module.exports = class SimpleRestContainer {
293
293
  debug(`current session context: ${util.inspect(this.view.context)}`)
294
294
  }
295
295
 
296
+ const _isAnyContextJsonPathMatch = (capName, capNameMatch) => {
297
+ const jsonPaths = getAllCapValues(capName, this.caps)
298
+ if (jsonPaths.length > 0) {
299
+ const jsonPathsMatch = getAllCapValues(capNameMatch, this.caps)
300
+ for (const [index, jsonPath] of jsonPaths.entries()) {
301
+ const contextNodes = jp.query(this.view.context, jsonPath)
302
+ if (_.isArray(contextNodes) && contextNodes.length > 0) {
303
+ if (jsonPathsMatch[index]) {
304
+ if (contextNodes[0] === jsonPathsMatch[index]) {
305
+ return {
306
+ jsonPath,
307
+ match: contextNodes[0]
308
+ }
309
+ }
310
+ } else {
311
+ return {
312
+ jsonPath
313
+ }
314
+ }
315
+ }
316
+ }
317
+ }
318
+ return null
319
+ }
320
+
321
+ const ignoreMatch = _isAnyContextJsonPathMatch(Capabilities.SIMPLEREST_CONTEXT_IGNORE_JSONPATH, Capabilities.SIMPLEREST_CONTEXT_IGNORE_MATCH)
322
+ if (ignoreMatch) {
323
+ if (ignoreMatch.match) debug(`ignoring response for context match: ${ignoreMatch.jsonPath} = ${ignoreMatch.match}`)
324
+ else debug(`ignoring response for context: ${ignoreMatch.jsonPath}`)
325
+ return
326
+ }
327
+
328
+ const skipMatch = _isAnyContextJsonPathMatch(Capabilities.SIMPLEREST_CONTEXT_SKIP_JSONPATH, Capabilities.SIMPLEREST_CONTEXT_SKIP_MATCH)
329
+ if (skipMatch) {
330
+ if (skipMatch.match) debug(`skipping response for context match: ${skipMatch.jsonPath} = ${skipMatch.match}`)
331
+ else debug(`skipping response for context: ${skipMatch.jsonPath}`)
332
+
333
+ setTimeout(() => this._doRequest({ messageText: '' }, true, true), 0)
334
+ return
335
+ }
336
+
296
337
  const result = []
297
338
  if (isFromUser) {
298
339
  const _extractFrom = (root, jsonPaths) => {
@@ -442,6 +483,14 @@ module.exports = class SimpleRestContainer {
442
483
  }
443
484
  }
444
485
  }
486
+
487
+ const continueMatch = _isAnyContextJsonPathMatch(Capabilities.SIMPLEREST_CONTEXT_CONTINUE_JSONPATH, Capabilities.SIMPLEREST_CONTEXT_CONTINUE_MATCH)
488
+ if (continueMatch) {
489
+ if (continueMatch.match) debug(`continue with next response for context match: ${continueMatch.jsonPath} = ${continueMatch.match}`)
490
+ else debug(`continue with next response for context: ${continueMatch.jsonPath}`)
491
+
492
+ setTimeout(() => this._doRequest({ messageText: '' }, true, true), 0)
493
+ }
445
494
  return result
446
495
  }
447
496
 
@@ -1,7 +1,6 @@
1
1
  const util = require('util')
2
2
  const _ = require('lodash')
3
3
  const debug = require('debug')('botium-core-Convo')
4
- const promiseRetry = require('promise-retry')
5
4
 
6
5
  const BotiumMockMessage = require('../mocks/BotiumMockMessage')
7
6
  const Capabilities = require('../Capabilities')
@@ -9,7 +8,6 @@ const Events = require('../Events')
9
8
  const ScriptingMemory = require('./ScriptingMemory')
10
9
  const { BotiumError, botiumErrorFromErr, botiumErrorFromList } = require('./BotiumError')
11
10
  const { normalizeText, toString, removeBuffers, splitStringInNonEmptyLines } = require('./helper')
12
- const RetryHelper = require('../helpers/RetryHelper')
13
11
 
14
12
  const { LOGIC_HOOK_INCLUDE } = require('./logichook/LogicHookConsts')
15
13
 
@@ -212,31 +210,6 @@ class Convo {
212
210
  }
213
211
 
214
212
  async Run (container) {
215
- if (container.caps.RETRY_CONVO_ASYNC) {
216
- return this.RunImpl(container).catch(err => {
217
- debug(`Convo failed with error "${err.message || JSON.stringify(err)}".`)
218
- throw err
219
- })
220
- } else {
221
- const retryHelper = new RetryHelper(container.caps, 'CONVO')
222
- return promiseRetry(async (retry, number) => {
223
- const retryRemaining = retryHelper.retrySettings.retries - number + 1
224
- return this.RunImpl(container).catch(err => {
225
- if (retryHelper.shouldRetry(err)) {
226
- debug(`Convo failed with error "${err.message || JSON.stringify(err)}". Retry ${retryRemaining > 0 ? 'enabled' : 'disabled'} (remaining #${retryRemaining}/${retryHelper.retrySettings.retries}, criterion matches)`)
227
- retry(err)
228
- } else {
229
- if (retryHelper.retryErrorPatterns.length > 0) {
230
- debug(`Convo failed with error "${err.message || JSON.stringify(err)}". Retry 'disabled' (remaining (#${retryRemaining}/${retryHelper.retrySettings.retries}), criterion does not match)`)
231
- }
232
- throw err
233
- }
234
- })
235
- }, retryHelper.retrySettings)
236
- }
237
- }
238
-
239
- async RunImpl (container) {
240
213
  const transcript = new Transcript({
241
214
  steps: [],
242
215
  attachments: [],
@@ -305,6 +278,7 @@ class Convo {
305
278
  for (let i = 0; i < this.conversation.length; i++) {
306
279
  const convoStep = this.conversation[i]
307
280
  const currentStepIndex = i
281
+ container.eventEmitter.emit(Events.CONVO_STEP_NEXT, container, convoStep, i)
308
282
  skipTranscriptStep = false
309
283
  const transcriptStep = new TranscriptStep({
310
284
  expected: new BotiumMockMessage(convoStep),
@@ -430,6 +404,15 @@ class Convo {
430
404
  throw failErr
431
405
  }
432
406
 
407
+ if (convoStep.skip === true) {
408
+ skipTranscriptStep = true
409
+ const nextConvoStep = this.conversation[i + 1]
410
+ if (nextConvoStep && nextConvoStep.sender === 'bot') {
411
+ waitForBotSays = false
412
+ }
413
+ continue
414
+ }
415
+
433
416
  if (!botMsg || (!botMsg.messageText && !botMsg.media && !botMsg.buttons && !botMsg.cards && !botMsg.sourceData && !botMsg.nlp)) {
434
417
  const failErr = new BotiumError(`${this.header.name}/${convoStep.stepTag}: bot says nothing`)
435
418
  debug(failErr)
@@ -19,6 +19,7 @@ const { BotiumError, botiumErrorFromList, botiumErrorFromErr } = require('./Boti
19
19
  const RetryHelper = require('../helpers/RetryHelper')
20
20
  const { getMatchFunction } = require('./MatchFunctions')
21
21
  const precompilers = require('./precompilers')
22
+ const { calculateWer, toPercent } = require('./helper')
22
23
 
23
24
  const globPattern = '**/+(*.convo.txt|*.utterances.txt|*.pconvo.txt|*.scriptingmemory.txt|*.xlsx|*.xlsm|*.convo.csv|*.pconvo.csv|*.utterances.csv|*.yaml|*.yml|*.json|*.md|*.markdown)'
24
25
  const skipPattern = /^skip[.\-_]/i
@@ -137,32 +138,57 @@ module.exports = class ScriptingProvider {
137
138
  const found = _.find(tomatch, (utt) => this.matchFn(botresponse, utt, this.caps[Capabilities.SCRIPTING_MATCHING_MODE_ARGS]))
138
139
  const asserterType = this.caps[Capabilities.SCRIPTING_MATCHING_MODE] === 'wer' ? 'Word Error Rate Asserter' : 'Text Match Asserter'
139
140
  if (_.isNil(found)) {
140
- let message = `${stepTag}: Bot response `
141
- message += meMsg ? `(on ${meMsg}) ` : ''
142
- message += botresponse ? ('"' + botresponse + '"') : '<no response>'
143
- message += ' expected to match '
144
- message += tomatch && tomatch.length > 1 ? 'one of ' : ''
145
- message += `${tomatch.map(e => e ? '"' + e + '"' : '<any response>').join(', ')}`
146
- throw new BotiumError(
147
- message,
148
- {
149
- type: 'asserter',
150
- source: asserterType,
151
- params: {
152
- matchingMode: this.caps[Capabilities.SCRIPTING_MATCHING_MODE],
153
- args: this.caps[Capabilities.SCRIPTING_MATCHING_MODE_ARGS] || null
154
- },
155
- context: {
156
- stepTag
157
- },
158
- cause: {
159
- expected: tomatch,
160
- actual: botresponse,
161
- matchingMode: this.caps[Capabilities.SCRIPTING_MATCHING_MODE],
162
- args: this.caps[Capabilities.SCRIPTING_MATCHING_MODE_ARGS] || null
141
+ if (this.caps[Capabilities.SCRIPTING_MATCHING_MODE] === 'wer') {
142
+ const wer = calculateWer(botresponse, tomatch[0])
143
+ const werArgs = this.caps[Capabilities.SCRIPTING_MATCHING_MODE_ARGS]
144
+ const threshold = ([',', '.'].find(p => `${werArgs[0]}`.includes(p)) ? parseFloat(werArgs[0]) : parseInt(werArgs[0]) / 100)
145
+ const message = `${stepTag}: Word Error Rate (${toPercent(wer)}) higher than accepted (${toPercent(threshold)})`
146
+ throw new BotiumError(
147
+ message,
148
+ {
149
+ type: 'asserter',
150
+ source: asserterType,
151
+ params: {
152
+ matchingMode: this.caps[Capabilities.SCRIPTING_MATCHING_MODE],
153
+ args: this.caps[Capabilities.SCRIPTING_MATCHING_MODE_ARGS] || null
154
+ },
155
+ context: {
156
+ stepTag
157
+ },
158
+ cause: {
159
+ expected: `<=${toPercent(threshold)} (${tomatch})`,
160
+ actual: `${toPercent(wer)} (${botresponse})`
161
+ }
163
162
  }
164
- }
165
- )
163
+ )
164
+ } else {
165
+ let message = `${stepTag}: Bot response `
166
+ message += meMsg ? `(on ${meMsg}) ` : ''
167
+ message += botresponse ? ('"' + botresponse + '"') : '<no response>'
168
+ message += ' expected to match '
169
+ message += tomatch && tomatch.length > 1 ? 'one of ' : ''
170
+ message += `${tomatch.map(e => e ? '"' + e + '"' : '<any response>').join(', ')}`
171
+ throw new BotiumError(
172
+ message,
173
+ {
174
+ type: 'asserter',
175
+ source: asserterType,
176
+ params: {
177
+ matchingMode: this.caps[Capabilities.SCRIPTING_MATCHING_MODE],
178
+ args: this.caps[Capabilities.SCRIPTING_MATCHING_MODE_ARGS] || null
179
+ },
180
+ context: {
181
+ stepTag
182
+ },
183
+ cause: {
184
+ expected: tomatch,
185
+ actual: botresponse,
186
+ matchingMode: this.caps[Capabilities.SCRIPTING_MATCHING_MODE],
187
+ args: this.caps[Capabilities.SCRIPTING_MATCHING_MODE_ARGS] || null
188
+ }
189
+ }
190
+ )
191
+ }
166
192
  }
167
193
  },
168
194
  assertBotNotResponse: (botresponse, nottomatch, stepTag, meMsg) => {
@@ -173,33 +199,58 @@ module.exports = class ScriptingProvider {
173
199
  const found = _.find(nottomatch, (utt) => this.matchFn(botresponse, utt, this.caps[Capabilities.SCRIPTING_MATCHING_MODE_ARGS]))
174
200
  const asserterType = this.caps[Capabilities.SCRIPTING_MATCHING_MODE] === 'wer' ? 'Word Error Rate Asserter' : 'Text Match Asserter'
175
201
  if (!_.isNil(found)) {
176
- let message = `${stepTag}: Bot response `
177
- message += meMsg ? `(on ${meMsg}) ` : ''
178
- message += botresponse ? ('"' + botresponse + '"') : '<no response>'
179
- message += ' expected NOT to match '
180
- message += nottomatch && nottomatch.length > 1 ? 'one of ' : ''
181
- message += `${nottomatch.map(e => e ? '"' + e + '"' : '<any response>').join(', ')}`
182
- throw new BotiumError(
183
- message,
184
- {
185
- type: 'asserter',
186
- source: asserterType,
187
- params: {
188
- matchingMode: this.caps[Capabilities.SCRIPTING_MATCHING_MODE],
189
- args: this.caps[Capabilities.SCRIPTING_MATCHING_MODE_ARGS] || null
190
- },
191
- context: {
192
- stepTag
193
- },
194
- cause: {
195
- not: true,
196
- expected: nottomatch,
197
- actual: botresponse,
198
- matchingMode: this.caps[Capabilities.SCRIPTING_MATCHING_MODE],
199
- args: this.caps[Capabilities.SCRIPTING_MATCHING_MODE_ARGS] || null
202
+ if (this.caps[Capabilities.SCRIPTING_MATCHING_MODE] === 'wer') {
203
+ const wer = calculateWer(botresponse, nottomatch[0])
204
+ const werArgs = this.caps[Capabilities.SCRIPTING_MATCHING_MODE_ARGS]
205
+ const threshold = ([',', '.'].find(p => `${werArgs[0]}`.includes(p)) ? parseFloat(werArgs[0]) : parseInt(werArgs[0]) / 100)
206
+ const message = `${stepTag}: Word Error Rate (${toPercent(wer)}) lower than accepted (${toPercent(threshold)})`
207
+ throw new BotiumError(
208
+ message,
209
+ {
210
+ type: 'asserter',
211
+ source: asserterType,
212
+ params: {
213
+ matchingMode: this.caps[Capabilities.SCRIPTING_MATCHING_MODE],
214
+ args: this.caps[Capabilities.SCRIPTING_MATCHING_MODE_ARGS] || null
215
+ },
216
+ context: {
217
+ stepTag
218
+ },
219
+ cause: {
220
+ expected: `>=${toPercent(threshold)} (${nottomatch})`,
221
+ actual: `${toPercent(wer)} (${botresponse})`
222
+ }
200
223
  }
201
- }
202
- )
224
+ )
225
+ } else {
226
+ let message = `${stepTag}: Bot response `
227
+ message += meMsg ? `(on ${meMsg}) ` : ''
228
+ message += botresponse ? ('"' + botresponse + '"') : '<no response>'
229
+ message += ' expected NOT to match '
230
+ message += nottomatch && nottomatch.length > 1 ? 'one of ' : ''
231
+ message += `${nottomatch.map(e => e ? '"' + e + '"' : '<any response>').join(', ')}`
232
+ throw new BotiumError(
233
+ message,
234
+ {
235
+ type: 'asserter',
236
+ source: asserterType,
237
+ params: {
238
+ matchingMode: this.caps[Capabilities.SCRIPTING_MATCHING_MODE],
239
+ args: this.caps[Capabilities.SCRIPTING_MATCHING_MODE_ARGS] || null
240
+ },
241
+ context: {
242
+ stepTag
243
+ },
244
+ cause: {
245
+ not: true,
246
+ expected: nottomatch,
247
+ actual: botresponse,
248
+ matchingMode: this.caps[Capabilities.SCRIPTING_MATCHING_MODE],
249
+ args: this.caps[Capabilities.SCRIPTING_MATCHING_MODE_ARGS] || null
250
+ }
251
+ }
252
+ )
253
+ }
203
254
  }
204
255
  },
205
256
  fail: null
@@ -889,10 +940,16 @@ module.exports = class ScriptingProvider {
889
940
  convoFilter: null
890
941
  }, options)
891
942
  const expandedConvos = []
943
+ // The globalContext is going to keep the data even if the Object.assign which happening to create the myContext in _expandConvo function
944
+ const context = {
945
+ globalContext: {
946
+ totalConvoCount: 0
947
+ }
948
+ }
892
949
  debug(`ExpandConvos - Using utterances expansion mode: ${this.caps[Capabilities.SCRIPTING_UTTEXPANSION_MODE]}`)
893
950
  this.convos.forEach((convo) => {
894
951
  convo.expandPartialConvos()
895
- for (const expanded of this._expandConvo(convo, options, {})) {
952
+ for (const expanded of this._expandConvo(convo, options, context)) {
896
953
  expanded.header.assertionCount = this.GetAssertionCount(expanded)
897
954
  if (options.justHeader) {
898
955
  const ConvoWithOnlyHeader = {
@@ -908,6 +965,7 @@ module.exports = class ScriptingProvider {
908
965
  }
909
966
  })
910
967
  this.convos = expandedConvos
968
+ this.totalConvoCount = context.globalContext.totalConvoCount
911
969
  if (!options.justHeader) {
912
970
  this._sortConvos()
913
971
  } else {
@@ -920,17 +978,24 @@ module.exports = class ScriptingProvider {
920
978
  // drop unwanted convos
921
979
  convoFilter: null
922
980
  }, options)
981
+ // The globalContext is going to keep the data even if the Object.assign which happening to create the myContext in _expandConvo function
982
+ const context = {
983
+ globalContext: {
984
+ totalConvoCount: 0
985
+ }
986
+ }
923
987
  debug(`ExpandConvos - Using utterances expansion mode: ${this.caps[Capabilities.SCRIPTING_UTTEXPANSION_MODE]}`)
924
988
  // creating a nested generator, calling the other.
925
989
  // We hope this.convos does not changes while this iterator is used
926
990
  const _convosIterable = function * (options) {
927
991
  for (const convo of this.convos) {
928
992
  convo.expandPartialConvos()
929
- yield * this._expandConvo(convo, options, {})
993
+ yield * this._expandConvo(convo, options, context)
930
994
  }
931
995
  }.bind(this)
932
996
 
933
997
  this.convosIterable = _convosIterable(options)
998
+ this.totalConvoCount = context.globalContext.totalConvoCount
934
999
  }
935
1000
 
936
1001
  /**
@@ -1122,6 +1187,9 @@ module.exports = class ScriptingProvider {
1122
1187
  }
1123
1188
  } else {
1124
1189
  const expanded = Object.assign(_.cloneDeep(currentConvo), { conversation: _.cloneDeep(convoStepsStack) })
1190
+ if (!_.isNil(_.get(context, 'globalContext.totalConvoCount'))) {
1191
+ context.globalContext.totalConvoCount++
1192
+ }
1125
1193
  if (!options.convoFilter || options.convoFilter(expanded)) {
1126
1194
  yield expanded
1127
1195
  }
@@ -553,6 +553,10 @@ const calculateWer = (str, pattern) => {
553
553
  const botMessageWords = botMessage.split(' ').map(bm => bm.trim())
554
554
  const utt = _prepareString(utterance)
555
555
 
556
+ // if no wildcards, just calculate WER
557
+ if (utt.indexOf('*') === -1) return speechScorer.wordErrorRate(botMessage, utt).toFixed(2)
558
+
559
+ // if there are wildcards, calculate WER for each wildcard part
556
560
  const errors = []
557
561
  for (let wildcardPart of utt.split('*')) {
558
562
  let wer = 1
@@ -572,7 +576,7 @@ const calculateWer = (str, pattern) => {
572
576
  }
573
577
  }
574
578
  if (_.isNil(subsetPhraseFound)) {
575
- throw new Error('Word Error Asserter: Something went wrong here, please try to modify your assertion!')
579
+ throw new Error('Word Error Asserter: When using wild cards, please make sure that the length of the asserter text is smaller than the bot message!')
576
580
  }
577
581
  errors.push(_getErrors(_getWords(wildcardPart), _getWords(subsetPhraseFound)))
578
582
  }
@@ -586,6 +590,8 @@ const calculateWer = (str, pattern) => {
586
590
  return (errCount / allCount).toFixed(2)
587
591
  }
588
592
 
593
+ const toPercent = (s) => `${(s * 100).toFixed(0)}%`
594
+
589
595
  module.exports = {
590
596
  normalizeText,
591
597
  splitStringInNonEmptyLines,
@@ -600,5 +606,6 @@ module.exports = {
600
606
  validateSender,
601
607
  validateConvo,
602
608
  linesToScriptingMemories,
603
- calculateWer
609
+ calculateWer,
610
+ toPercent
604
611
  }
@@ -1,3 +1,4 @@
1
+ const _ = require('lodash')
1
2
  const { SCRIPTING_NORMALIZE_TEXT } = require('../../../Capabilities')
2
3
  const { BotiumError } = require('../../BotiumError')
3
4
  const { buttonsFromMsg } = require('../helpers')
@@ -11,17 +12,29 @@ module.exports = class ButtonsAsserter {
11
12
  }
12
13
 
13
14
  _evalButtons (args, botMsg) {
14
- const allButtons = buttonsFromMsg(botMsg, true).map(b => b.text).filter(b => b).map(b => normalizeText(b, !!this.caps[SCRIPTING_NORMALIZE_TEXT]))
15
+ const allButtons = buttonsFromMsg(botMsg, true).map(b => ({ text: b.text, payload: b.payload })).filter(b => b).map(b => ({ text: normalizeText(b.text, !!this.caps[SCRIPTING_NORMALIZE_TEXT]), payload: b.payload }))
15
16
  if (!args || args.length === 0) {
16
- return { allButtons, buttonsNotFound: [], buttonsFound: allButtons }
17
+ return { allButtons, buttonsNotFound: [], buttonsFound: allButtons.map(b => b.text) }
17
18
  }
18
19
  const buttonsNotFound = []
19
20
  const buttonsFound = []
21
+ const parsePayload = (payload) => {
22
+ if (_.isNil(payload)) {
23
+ return undefined
24
+ }
25
+ try {
26
+ return JSON.parse(payload)
27
+ } catch (e) {
28
+ return payload
29
+ }
30
+ }
20
31
  for (let i = 0; i < (args || []).length; i++) {
21
- if (allButtons.findIndex(b => this.context.Match(b, normalizeText(args[i], !!this.caps[SCRIPTING_NORMALIZE_TEXT]))) < 0) {
22
- buttonsNotFound.push(args[i])
23
- } else {
32
+ const matchByText = allButtons.some(b => this.context.Match(b.text, normalizeText(args[i], !!this.caps[SCRIPTING_NORMALIZE_TEXT])))
33
+ const matchByPayload = allButtons.some(b => _.isEqual(parsePayload(b.payload), parsePayload(args[i])))
34
+ if (matchByText || matchByPayload) {
24
35
  buttonsFound.push(args[i])
36
+ } else {
37
+ buttonsNotFound.push(args[i])
25
38
  }
26
39
  }
27
40
  return { allButtons, buttonsNotFound, buttonsFound }
@@ -42,7 +55,7 @@ module.exports = class ButtonsAsserter {
42
55
  cause: {
43
56
  not: true,
44
57
  expected: args,
45
- actual: allButtons,
58
+ actual: JSON.stringify(allButtons, null, 2),
46
59
  diff: buttonsFound
47
60
  }
48
61
  }
@@ -67,7 +80,7 @@ module.exports = class ButtonsAsserter {
67
80
  cause: {
68
81
  not: false,
69
82
  expected: args,
70
- actual: allButtons,
83
+ actual: JSON.stringify(allButtons, null, 2),
71
84
  diff: buttonsNotFound
72
85
  }
73
86
  }
@@ -85,7 +98,7 @@ module.exports = class ButtonsAsserter {
85
98
  cause: {
86
99
  not: false,
87
100
  expected: args,
88
- actual: allButtons,
101
+ actual: JSON.stringify(allButtons, null, 2),
89
102
  diff: buttonsNotFound
90
103
  }
91
104
  }