botium-core 1.14.1 → 1.14.4

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 (39) hide show
  1. package/dist/botium-cjs.js +341 -126
  2. package/dist/botium-cjs.js.map +1 -1
  3. package/dist/botium-es.js +359 -144
  4. package/dist/botium-es.js.map +1 -1
  5. package/package.json +11 -11
  6. package/src/Capabilities.js +2 -0
  7. package/src/scripting/BotiumError.js +40 -3
  8. package/src/scripting/CompilerMarkdown.js +2 -1
  9. package/src/scripting/CompilerTxt.js +1 -4
  10. package/src/scripting/Convo.js +113 -19
  11. package/src/scripting/MatchFunctions.js +30 -8
  12. package/src/scripting/ScriptingMemory.js +7 -0
  13. package/src/scripting/ScriptingProvider.js +87 -36
  14. package/src/scripting/helper.js +3 -2
  15. package/src/scripting/logichook/LogicHookConsts.js +5 -2
  16. package/src/scripting/logichook/LogicHookUtils.js +8 -6
  17. package/src/scripting/logichook/logichooks/ConvoStepParametersLogicHook.js +6 -0
  18. package/src/scripting/logichook/logichooks/OrderedListToButtonLogicHook.js +37 -0
  19. package/test/compiler/compilermarkdown.spec.js +3 -3
  20. package/test/compiler/compilertxt.spec.js +1 -1
  21. package/test/compiler/convos/txt/convos_emptyrow_just_emptyrow.convo.txt +1 -1
  22. package/test/convo/fillAndApplyScriptingMemory.spec.js +11 -0
  23. package/test/logichooks/orderedListToButton.spec.js +35 -0
  24. package/test/scripting/asserters/convoStepParameters.spec.js +151 -0
  25. package/test/scripting/asserters/convos/TEXT_GOOD.convo.txt +6 -0
  26. package/test/scripting/asserters/convos/convo_step_parameter_matchmode_failed.convo.txt +8 -0
  27. package/test/scripting/asserters/convos/convo_step_parameter_matchmode_failed_wer.convo.txt +9 -0
  28. package/test/scripting/asserters/convos/convo_step_parameter_retry_asserters_all_good.convo.txt +9 -0
  29. package/test/scripting/asserters/convos/convo_step_parameter_retry_asserters_botium_timeout.convo.txt +9 -0
  30. package/test/scripting/asserters/convos/convo_step_parameter_retry_asserters_good.convo.txt +9 -0
  31. package/test/scripting/asserters/convos/convo_step_parameter_retry_asserters_good_global.convo.txt +9 -0
  32. package/test/scripting/asserters/convos/convo_step_parameter_retry_main_and_asserter.convo.txt +10 -0
  33. package/test/scripting/asserters/convos/convo_step_parameter_retry_main_botium_timeout.convo.txt +9 -0
  34. package/test/scripting/asserters/convos/convo_step_parameter_retry_main_but_no_button.convo.txt +10 -0
  35. package/test/scripting/asserters/convos/convo_step_parameter_retry_main_good.convo.txt +9 -0
  36. package/test/scripting/asserters/convos/convo_step_parameter_retry_main_good_begin.convo.txt +11 -0
  37. package/test/scripting/logichooks/convos/scripting_memory_overwrite_and_check.convo.txt +2 -2
  38. package/test/scripting/matching/matchingmode.spec.js +4 -1
  39. package/test/scripting/scriptingProvider.spec.js +38 -12
@@ -92,37 +92,37 @@ module.exports = class ScriptingProvider {
92
92
 
93
93
  this.scriptingEvents = {
94
94
  onConvoBegin: ({ convo, convoStep, scriptingMemory, ...rest }) => {
95
- return this._createLogicHookPromises({ hookType: 'onConvoBegin', logicHooks: (convo.beginLogicHook || []), convo, convoStep, scriptingMemory, ...rest })
95
+ return this._createLogicHookPromises({ hookType: 'onConvoBegin', logicHooks: (convo?.beginLogicHook || []), convo, convoStep, scriptingMemory, ...rest })
96
96
  },
97
97
  onConvoEnd: ({ convo, convoStep, scriptingMemory, ...rest }) => {
98
- return this._createLogicHookPromises({ hookType: 'onConvoEnd', logicHooks: (convo.endLogicHook || []), convo, convoStep, scriptingMemory, ...rest })
98
+ return this._createLogicHookPromises({ hookType: 'onConvoEnd', logicHooks: (convo?.endLogicHook || []), convo, convoStep, scriptingMemory, ...rest })
99
99
  },
100
100
  onMeStart: ({ convo, convoStep, scriptingMemory, ...rest }) => {
101
- return this._createLogicHookPromises({ hookType: 'onMeStart', logicHooks: (convoStep.logicHooks || []), convo, convoStep, scriptingMemory, ...rest })
101
+ return this._createLogicHookPromises({ hookType: 'onMeStart', logicHooks: (convoStep?.logicHooks || []), convo, convoStep, scriptingMemory, ...rest })
102
102
  },
103
103
  onMePrepare: ({ convo, convoStep, scriptingMemory, ...rest }) => {
104
- return this._createLogicHookPromises({ hookType: 'onMePrepare', logicHooks: (convoStep.logicHooks || []), convo, convoStep, scriptingMemory, ...rest })
104
+ return this._createLogicHookPromises({ hookType: 'onMePrepare', logicHooks: (convoStep?.logicHooks || []), convo, convoStep, scriptingMemory, ...rest })
105
105
  },
106
106
  onMeEnd: ({ convo, convoStep, scriptingMemory, ...rest }) => {
107
- return this._createLogicHookPromises({ hookType: 'onMeEnd', logicHooks: (convoStep.logicHooks || []), convo, convoStep, scriptingMemory, ...rest })
107
+ return this._createLogicHookPromises({ hookType: 'onMeEnd', logicHooks: (convoStep?.logicHooks || []), convo, convoStep, scriptingMemory, ...rest })
108
108
  },
109
109
  onBotStart: ({ convo, convoStep, scriptingMemory, ...rest }) => {
110
- return this._createLogicHookPromises({ hookType: 'onBotStart', logicHooks: (convoStep.logicHooks || []), convo, convoStep, scriptingMemory, ...rest })
110
+ return this._createLogicHookPromises({ hookType: 'onBotStart', logicHooks: (convoStep?.logicHooks || []), convo, convoStep, scriptingMemory, ...rest })
111
111
  },
112
112
  onBotPrepare: ({ convo, convoStep, scriptingMemory, ...rest }) => {
113
- return this._createLogicHookPromises({ hookType: 'onBotPrepare', logicHooks: (convoStep.logicHooks || []), convo, convoStep, scriptingMemory, ...rest })
113
+ return this._createLogicHookPromises({ hookType: 'onBotPrepare', logicHooks: (convoStep?.logicHooks || []), convo, convoStep, scriptingMemory, ...rest })
114
114
  },
115
115
  onBotEnd: ({ convo, convoStep, scriptingMemory, ...rest }) => {
116
- return this._createLogicHookPromises({ hookType: 'onBotEnd', logicHooks: (convoStep.logicHooks || []), convo, convoStep, scriptingMemory, ...rest })
116
+ return this._createLogicHookPromises({ hookType: 'onBotEnd', logicHooks: (convoStep?.logicHooks || []), convo, convoStep, scriptingMemory, ...rest })
117
117
  },
118
118
  assertConvoBegin: ({ convo, convoStep, scriptingMemory, ...rest }) => {
119
- return this._createAsserterPromises({ asserterType: 'assertConvoBegin', asserters: (convo.beginAsserter || []), convo, convoStep, scriptingMemory, ...rest })
119
+ return this._createAsserterPromises({ asserterType: 'assertConvoBegin', asserters: (convo?.beginAsserter || []), convo, convoStep, scriptingMemory, ...rest })
120
120
  },
121
121
  assertConvoStep: ({ convo, convoStep, scriptingMemory, ...rest }) => {
122
- return this._createAsserterPromises({ asserterType: 'assertConvoStep', asserters: (convoStep.asserters || []), convo, convoStep, scriptingMemory, ...rest })
122
+ return this._createAsserterPromises({ asserterType: 'assertConvoStep', asserters: (convoStep?.asserters || []), convo, convoStep, scriptingMemory, ...rest })
123
123
  },
124
124
  assertConvoEnd: ({ convo, convoStep, scriptingMemory, ...rest }) => {
125
- return this._createAsserterPromises({ asserterType: 'assertConvoEnd', asserters: (convo.endAsserter || []), convo, convoStep, scriptingMemory, ...rest })
125
+ return this._createAsserterPromises({ asserterType: 'assertConvoEnd', asserters: (convo?.endAsserter || []), convo, convoStep, scriptingMemory, ...rest })
126
126
  },
127
127
  setUserInput: ({ convo, convoStep, scriptingMemory, ...rest }) => {
128
128
  return this._createUserInputPromises({ convo, convoStep, scriptingMemory, ...rest })
@@ -130,18 +130,20 @@ module.exports = class ScriptingProvider {
130
130
  resolveUtterance: ({ utterance, resolveEmptyIfUnknown }) => {
131
131
  return this._resolveUtterance({ utterance, resolveEmptyIfUnknown })
132
132
  },
133
- assertBotResponse: (botresponse, tomatch, stepTag, meMsg) => {
133
+ assertBotResponse: (botresponse, tomatch, stepTag, meMsg, convoStepParameters = {}) => {
134
134
  if (!_.isArray(tomatch)) {
135
135
  tomatch = [tomatch]
136
136
  }
137
137
  debug(`assertBotResponse ${stepTag} ${meMsg ? `(${meMsg}) ` : ''}BOT: ${botresponse} = ${tomatch} ...`)
138
- const found = _.find(tomatch, (utt) => this.matchFn(botresponse, utt, this.caps[Capabilities.SCRIPTING_MATCHING_MODE_ARGS]))
138
+ const matchFn = convoStepParameters.matchingMode ? (getMatchFunction(convoStepParameters.matchingMode) || this.matchFn) : this.matchFn
139
+ const found = _.find(tomatch, (utt) => matchFn(botresponse, utt, this.caps[Capabilities.SCRIPTING_MATCHING_MODE_ARGS], convoStepParameters))
139
140
  const asserterType = this.caps[Capabilities.SCRIPTING_MATCHING_MODE] === 'wer' ? 'Word Error Rate Asserter' : 'Text Match Asserter'
140
141
  if (_.isNil(found)) {
141
- if (this.caps[Capabilities.SCRIPTING_MATCHING_MODE] === 'wer') {
142
+ const matchingMode = convoStepParameters.matchingMode || this.caps[Capabilities.SCRIPTING_MATCHING_MODE]
143
+ if (matchingMode === 'wer') {
142
144
  const wer = calculateWer(botresponse, tomatch[0])
143
145
  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)
146
+ const threshold = !_.isNil(convoStepParameters.matchingModeWer) ? (convoStepParameters.matchingModeWer / 100) : ([',', '.'].find(p => `${werArgs[0]}`.includes(p)) ? parseFloat(werArgs[0]) : parseInt(werArgs[0]) / 100)
145
147
  const message = `${stepTag}: Word Error Rate (${toPercent(wer)}) higher than accepted (${toPercent(threshold)})`
146
148
  throw new BotiumError(
147
149
  message,
@@ -149,7 +151,7 @@ module.exports = class ScriptingProvider {
149
151
  type: 'asserter',
150
152
  source: asserterType,
151
153
  params: {
152
- matchingMode: this.caps[Capabilities.SCRIPTING_MATCHING_MODE],
154
+ matchingMode,
153
155
  args: this.caps[Capabilities.SCRIPTING_MATCHING_MODE_ARGS] || null
154
156
  },
155
157
  context: {
@@ -191,18 +193,20 @@ module.exports = class ScriptingProvider {
191
193
  }
192
194
  }
193
195
  },
194
- assertBotNotResponse: (botresponse, nottomatch, stepTag, meMsg) => {
196
+ assertBotNotResponse: (botresponse, nottomatch, stepTag, meMsg, convoStepParameters = {}) => {
195
197
  if (!_.isArray(nottomatch)) {
196
198
  nottomatch = [nottomatch]
197
199
  }
198
200
  debug(`assertBotNotResponse ${stepTag} ${meMsg ? `(${meMsg}) ` : ''}BOT: ${botresponse} != ${nottomatch} ...`)
199
- const found = _.find(nottomatch, (utt) => this.matchFn(botresponse, utt, this.caps[Capabilities.SCRIPTING_MATCHING_MODE_ARGS]))
201
+ const matchFn = convoStepParameters.matchingMode ? (getMatchFunction(convoStepParameters.matchingMode) || this.matchFn) : this.matchFn
202
+ const found = _.find(nottomatch, (utt) => matchFn(botresponse, utt, this.caps[Capabilities.SCRIPTING_MATCHING_MODE_ARGS], convoStepParameters))
200
203
  const asserterType = this.caps[Capabilities.SCRIPTING_MATCHING_MODE] === 'wer' ? 'Word Error Rate Asserter' : 'Text Match Asserter'
201
204
  if (!_.isNil(found)) {
202
- if (this.caps[Capabilities.SCRIPTING_MATCHING_MODE] === 'wer') {
205
+ const matchingMode = convoStepParameters.matchingMode || this.caps[Capabilities.SCRIPTING_MATCHING_MODE]
206
+ if (matchingMode === 'wer') {
203
207
  const wer = calculateWer(botresponse, nottomatch[0])
204
208
  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)
209
+ const threshold = !_.isNil(convoStepParameters.matchingModeWer) ? (convoStepParameters.matchingModeWer / 100) : ([',', '.'].find(p => `${werArgs[0]}`.includes(p)) ? parseFloat(werArgs[0]) : parseInt(werArgs[0]) / 100)
206
210
  const message = `${stepTag}: Word Error Rate (${toPercent(wer)}) lower than accepted (${toPercent(threshold)})`
207
211
  throw new BotiumError(
208
212
  message,
@@ -210,7 +214,7 @@ module.exports = class ScriptingProvider {
210
214
  type: 'asserter',
211
215
  source: asserterType,
212
216
  params: {
213
- matchingMode: this.caps[Capabilities.SCRIPTING_MATCHING_MODE],
217
+ matchingMode,
214
218
  args: this.caps[Capabilities.SCRIPTING_MATCHING_MODE_ARGS] || null
215
219
  },
216
220
  context: {
@@ -270,6 +274,22 @@ module.exports = class ScriptingProvider {
270
274
  assertConvoStep: 'assertNotConvoStep',
271
275
  assertConvoEnd: 'assertNotConvoEnd'
272
276
  }
277
+ const updateExceptionContext = (promise, asserter) => {
278
+ const updateError = (err) => {
279
+ if (err instanceof BotiumError) {
280
+ if (!err.context) {
281
+ err.context = {}
282
+ }
283
+
284
+ err.context.asserter = asserter.name
285
+
286
+ throw err
287
+ } else {
288
+ throw botiumErrorFromErr(_.isString(err) ? err : err.message, err, { asserter: asserter.name })
289
+ }
290
+ }
291
+ return promise.catch(err => updateError(err))
292
+ }
273
293
  const callAsserter = (asserterSpec, asserter, params) => {
274
294
  if (asserterSpec.not) {
275
295
  const notAsserterType = mapNot[asserterType]
@@ -301,18 +321,35 @@ module.exports = class ScriptingProvider {
301
321
 
302
322
  const convoAsserter = asserters
303
323
  .filter(a => this.asserters[a.name][asserterType])
304
- .map(a => callAsserter(a, this.asserters[a.name], {
305
- convo,
306
- convoStep,
307
- scriptingMemory,
308
- container,
309
- args: ScriptingMemory.applyToArgs(a.args, scriptingMemory, container.caps, rest.botMsg),
310
- isGlobal: false,
311
- ...rest
324
+ .map(a => ({
325
+ asserter: a,
326
+ promise: callAsserter(a, this.asserters[a.name], {
327
+ convo,
328
+ convoStep,
329
+ scriptingMemory,
330
+ container,
331
+ args: ScriptingMemory.applyToArgs(a.args, scriptingMemory, container.caps, rest.botMsg),
332
+ isGlobal: false,
333
+ ...rest
334
+ })
312
335
  }))
336
+ .map(({ promise, asserter }) => updateExceptionContext(promise, asserter))
337
+
313
338
  const globalAsserter = Object.values(this.globalAsserter)
314
339
  .filter(a => a[asserterType])
315
- .map(a => p(this.retryHelperAsserter, () => a[asserterType]({ convo, convoStep, scriptingMemory, container, args: [], isGlobal: true, ...rest })))
340
+ .map(a => ({
341
+ asserter: a,
342
+ promise: p(this.retryHelperAsserter, () => a[asserterType]({
343
+ convo,
344
+ convoStep,
345
+ scriptingMemory,
346
+ container,
347
+ args: [],
348
+ isGlobal: true,
349
+ ...rest
350
+ }))
351
+ }))
352
+ .map(({ promise, asserter }) => updateExceptionContext(promise, asserter))
316
353
 
317
354
  const allPromises = [...convoAsserter, ...globalAsserter]
318
355
  if (this.caps[Capabilities.SCRIPTING_ENABLE_MULTIPLE_ASSERT_ERRORS]) {
@@ -335,8 +372,10 @@ module.exports = class ScriptingProvider {
335
372
  throw Error(`Unknown hookType ${hookType}`)
336
373
  }
337
374
 
338
- const convoStepPromises = (logicHooks || [])
375
+ const localHooks = (logicHooks || [])
339
376
  .filter(l => this.logicHooks[l.name][hookType])
377
+
378
+ const convoStepPromises = localHooks
340
379
  .map(l => p(this.retryHelperLogicHook, () => this.logicHooks[l.name][hookType]({
341
380
  convo,
342
381
  convoStep,
@@ -347,17 +386,24 @@ module.exports = class ScriptingProvider {
347
386
  ...rest
348
387
  })))
349
388
 
350
- const globalPromises = Object.values(this.globalLogicHook)
351
- .filter(l => l[hookType])
352
- .map(l => p(this.retryHelperLogicHook, () => l[hookType]({ convo, convoStep, scriptingMemory, container, args: [], isGlobal: true, ...rest })))
389
+ const globalHooks = Object.values(this.globalLogicHook).filter(l => l[hookType])
390
+ const globalPromises = globalHooks.map(l => p(this.retryHelperLogicHook, () => l[hookType]({ convo, convoStep, scriptingMemory, container, args: [], isGlobal: true, ...rest })))
353
391
 
354
392
  const allPromises = [...convoStepPromises, ...globalPromises]
355
- if (allPromises.length > 0) return Promise.all(allPromises).then(() => true)
393
+
394
+ if (allPromises.length > 0) {
395
+ return Promise.all(allPromises).then(() => {
396
+ return {
397
+ // just returning some humanreadable
398
+ hooks: [...localHooks, ...globalHooks].map(h => h.name || h.context?.ref || JSON.stringify(h))
399
+ }
400
+ })
401
+ }
356
402
  return Promise.resolve(false)
357
403
  }
358
404
 
359
405
  _createUserInputPromises ({ convo, convoStep, scriptingMemory, container, ...rest }) {
360
- const convoStepPromises = (convoStep.userInputs || [])
406
+ const convoStepPromises = (convoStep?.userInputs || [])
361
407
  .filter(ui => this.userInputs[ui.name])
362
408
  .map(ui => p(this.retryHelperUserInput, () => this.userInputs[ui.name].setUserInput({
363
409
  convo,
@@ -424,6 +470,11 @@ module.exports = class ScriptingProvider {
424
470
  }
425
471
  }
426
472
 
473
+ // Livechat, and crawler using logichooks too. So they need script context
474
+ BuildScriptContext () {
475
+ return this._buildScriptContext()
476
+ }
477
+
427
478
  Build () {
428
479
  const CompilerXlsx = require('./CompilerXlsx')
429
480
  this.compilers[Constants.SCRIPTING_FORMAT_XSLX] = new CompilerXlsx(this._buildScriptContext(), this.caps)
@@ -4,6 +4,7 @@ const speechScorer = require('word-error-rate')
4
4
  const debug = require('debug')('botium-core-scripting-helper')
5
5
 
6
6
  const { E_SCRIPTING_MEMORY_COLUMN_MODE } = require('../Enums')
7
+ const WHITE_SPACES_EXCEPT_SPACE_CHAR_AT_THE_END = /[\n\t\r]+$/
7
8
 
8
9
  const normalizeText = (str, doCleanup) => {
9
10
  if (str && _.isArray(str)) {
@@ -247,7 +248,7 @@ const linesToConvoStep = (lines, sender, context, eol, singleLineMode = false) =
247
248
  if (eol === null) {
248
249
  throw new Error('eol cant be null')
249
250
  }
250
- convoStep.messageText = textLines.join(eol).trim()
251
+ convoStep.messageText = textLines.join(eol).replace(WHITE_SPACES_EXCEPT_SPACE_CHAR_AT_THE_END, '')
251
252
  }
252
253
  }
253
254
  } else {
@@ -473,7 +474,7 @@ const convoStepToLines = (step) => {
473
474
  lines.push(logicHook.name + _formatAppendArgs(logicHook.args))
474
475
  })
475
476
  }
476
- return lines.map(l => l.trim())
477
+ return lines
477
478
  }
478
479
 
479
480
  const linesToScriptingMemories = (lines, columnMode = null) => {
@@ -61,11 +61,14 @@ module.exports = {
61
61
  { name: 'CONDITIONAL_STEP_TIME_BASED', className: 'ConditionalTimeBasedLogicHook' },
62
62
  { name: 'CONDITIONAL_STEP_BUSINESS_HOURS', className: 'ConditionalBusinessHoursLogicHook' },
63
63
  { name: 'CONDITIONAL_STEP_CAPABILITY_VALUE_BASED', className: 'ConditionalCapabilityValueBasedLogicHook' },
64
- { name: 'CONDITIONAL_STEP_JSON_PATH_BASED', className: 'ConditionalJsonPathBasedLogicHook.js' }
64
+ { name: 'CONDITIONAL_STEP_JSON_PATH_BASED', className: 'ConditionalJsonPathBasedLogicHook.js' },
65
+ { name: 'CONVO_STEP_PARAMETERS', className: 'ConvoStepParametersLogicHook.js' },
66
+ { name: 'ORDERED_LIST_TO_BUTTON', className: 'OrderedListToButtonLogicHook' }
65
67
  ],
66
68
  DEFAULT_USER_INPUTS: [
67
69
  { name: 'BUTTON', className: 'ButtonInput' },
68
70
  { name: 'MEDIA', className: 'MediaInput' },
69
71
  { name: 'FORM', className: 'FormInput' }
70
- ]
72
+ ],
73
+ LOGIC_HOOK_EVENTS: ['onConvoBegin', 'onMeStart', 'onMePrepare', 'onMeEnd', 'onBotStart', 'onBotEnd', 'onBotPrepare', 'onConvoEnd']
71
74
  }
@@ -147,19 +147,21 @@ module.exports = class LogicHookUtils {
147
147
  }
148
148
  }
149
149
 
150
+ const typeAsText = hookType === 'asserter' ? 'Asserter' : hookType === 'logichook' ? 'Logic Hook' : hookType === 'userinput' ? 'User Input' : 'Unknown'
151
+
150
152
  if (isClass(src)) {
151
153
  try {
152
154
  const CheckClass = src
153
155
  return new CheckClass({ ref, ...this.buildScriptContext }, this.caps, args)
154
156
  } catch (err) {
155
- throw new Error(`Logic Hook specification ${ref} from class invalid: ${err.message}`)
157
+ throw new Error(`${typeAsText} specification ${ref} from class invalid: ${err.message}`)
156
158
  }
157
159
  }
158
160
  if (_.isFunction(src)) {
159
161
  try {
160
162
  return src({ ref, ...this.buildScriptContext }, this.caps, args)
161
163
  } catch (err) {
162
- throw new Error(`Logic Hook specification ${ref} from function invalid: ${err.message}`)
164
+ throw new Error(`${typeAsText} specification ${ref} from function invalid: ${err.message}`)
163
165
  }
164
166
  }
165
167
  if (_.isObject(src) && !_.isString(src)) {
@@ -177,7 +179,7 @@ module.exports = class LogicHookUtils {
177
179
  }, {})
178
180
  return hookObject
179
181
  } catch (err) {
180
- throw new Error(`Logic Hook specification ${ref} ${hookType} from provided src (${util.inspect(src)}) invalid: ${err.message}`)
182
+ throw new Error(`${typeAsText} specification ${ref} ${hookType} from provided src (${util.inspect(src)}) invalid: ${err.message}`)
181
183
  }
182
184
  }
183
185
 
@@ -230,7 +232,7 @@ module.exports = class LogicHookUtils {
230
232
  try {
231
233
  return tryLoadFromSource(tryLoadFile, tryLoad.tryLoadAsserterByName)
232
234
  } catch (err) {
233
- loadErr.push(`Logic Hook specification ${ref} ${hookType} from "${src}" invalid: ${err.message} `)
235
+ loadErr.push(`${typeAsText} specification ${ref} ${hookType} from "${src}" invalid: ${err.message} `)
234
236
  }
235
237
  }
236
238
  }
@@ -239,13 +241,13 @@ module.exports = class LogicHookUtils {
239
241
  try {
240
242
  return tryLoadFromSource(tryLoad.tryLoadPackageName, tryLoad.tryLoadAsserterByName)
241
243
  } catch (err) {
242
- loadErr.push(`Logic Hook specification ${ref} ${hookType} from "${src}" invalid: ${err.message} `)
244
+ loadErr.push(`${typeAsText} specification ${ref} ${hookType} from "${src}" invalid: ${err.message} `)
243
245
  }
244
246
  }
245
247
  }
246
248
 
247
249
  loadErr.forEach(debug)
248
250
  }
249
- throw new Error(`Logic Hook specification ${ref} ${hookType} from "${util.inspect(src)}" invalid : no loader available`)
251
+ throw new Error(`${typeAsText} specification ${ref} ${hookType} from "${util.inspect(src)}" invalid : no loader available`)
250
252
  }
251
253
  }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * This LogicHook is just a marker. It is used Convo Step parameters
3
+ * @type {module.ConvoStepParametersLogicHook}
4
+ */
5
+ module.exports = class ConvoStepParametersLogicHook {
6
+ }
@@ -0,0 +1,37 @@
1
+ const _ = require('lodash')
2
+ const PATTERN = '^\\s*(\\d+)\\.'
3
+ const debug = require('debug')('botium-core-OrderedListToButtonLogicHook')
4
+
5
+ module.exports = class OrderedListToButtonLogicHook {
6
+ constructor (context, caps = {}, globalArgs = {}) {
7
+ this.context = context
8
+ this.caps = caps
9
+ this.globalArgs = globalArgs
10
+ }
11
+
12
+ onBotPrepare ({ botMsg, args }) {
13
+ const pattern = args?.[0] || this.globalArgs?.pattern || PATTERN
14
+ let regexp
15
+ try {
16
+ regexp = new RegExp(pattern, 'gm')
17
+ } catch (err) {
18
+ throw new Error(`OrderedListToButtonLogicHook: regex is not valid: ${pattern} ${err.messageText}`)
19
+ }
20
+ const buttons = []
21
+ if (botMsg.messageText && _.isString(botMsg.messageText)) {
22
+ const matches = botMsg.messageText.matchAll(regexp)
23
+
24
+ for (const match of matches) {
25
+ if (match && match[1]) {
26
+ buttons.push({ text: match[1], payload: match[1] })
27
+ }
28
+ }
29
+ } else {
30
+ debug(`ConditionalBusinessHoursLogicHook onBotPrepare, msg has no messageText to check ${JSON.stringify(botMsg)}`)
31
+ }
32
+
33
+ if (buttons.length) {
34
+ botMsg.buttons = [...(botMsg.buttons || []), ...buttons]
35
+ }
36
+ }
37
+ }
@@ -161,7 +161,7 @@ describe('compiler.compilermarkdown', function () {
161
161
  const compiler = new Compiler(context, Object.assign({}, DefaultCapabilities, caps))
162
162
 
163
163
  compiler.Compile(scriptBuffer, 'SCRIPTING_TYPE_CONVO')
164
- assert.equal(context.convos[0].conversation[1].messageText, 'hello meat bag \n!hello2')
164
+ assert.equal(context.convos[0].conversation[1].messageText, 'hello meat bag\n!hello2')
165
165
  assert.equal(context.convos[0].conversation[1].not, true)
166
166
  })
167
167
  })
@@ -191,7 +191,7 @@ describe('compiler.compilermarkdown', function () {
191
191
  compiler.Compile(scriptBuffer, 'SCRIPTING_TYPE_CONVO')
192
192
  assert.fail('expected error')
193
193
  } catch (err) {
194
- assert.equal(err.message, 'Failed to parse conversation. All element in convo step has to be optional or not optional: ["?hello meat bag ","BUTTONS checkbutton|checkbutton2 "]')
194
+ assert.equal(err.message, 'Failed to parse conversation. All element in convo step has to be optional or not optional: ["?hello meat bag","BUTTONS checkbutton|checkbutton2"]')
195
195
  }
196
196
  })
197
197
  it('should read ?? as ?', async function () {
@@ -225,7 +225,7 @@ describe('compiler.compilermarkdown', function () {
225
225
  const compiler = new Compiler(context, Object.assign({}, DefaultCapabilities, caps))
226
226
 
227
227
  compiler.Compile(scriptBuffer, 'SCRIPTING_TYPE_CONVO')
228
- assert.equal(context.convos[0].conversation[1].messageText, 'hello meat bag \n?hello2')
228
+ assert.equal(context.convos[0].conversation[1].messageText, 'hello meat bag\n?hello2')
229
229
  assert.equal(context.convos[0].conversation[1].optional, true)
230
230
  })
231
231
  })
@@ -236,7 +236,7 @@ describe('compiler.compilertxt', function () {
236
236
  assert.equal(convo.conversation.length, 2)
237
237
  assert.equal(convo.conversation[0].messageText, '')
238
238
  assert.equal(convo.conversation[0].logicHooks.length, 0)
239
- assert.equal(convo.conversation[1].messageText, 'Hi')
239
+ assert.equal(convo.conversation[1].messageText, 'Hi ')
240
240
  })
241
241
  it('should read nothing if there is nothing (even no separator)', async function () {
242
242
  const scriptBuffer = fs.readFileSync(path.resolve(__dirname, CONVOS_DIR, 'convos_emptyrow_no_separator_row.convo.txt'))
@@ -817,6 +817,17 @@ describe('convo.fillAndApplyScriptingMemory', function () {
817
817
 
818
818
  assert.equal(result, moment().subtract(1, 'month').format('YYYY.MM.DD'))
819
819
  })
820
+ it('date_add_dynamic', async function () {
821
+ const result = ScriptingMemory.apply(
822
+ { caps: CAPS_ENABLE_SCRIPTING_MEMORY },
823
+ {
824
+ days: 2
825
+ },
826
+ '$date_add($days, "day", YYYY.MM.DD)'
827
+ )
828
+
829
+ assert.equal(result, moment().add(2, 'day').format('YYYY.MM.DD'))
830
+ })
820
831
 
821
832
  it('random', async function () {
822
833
  const result = ScriptingMemory.apply(
@@ -0,0 +1,35 @@
1
+ const assert = require('chai').assert
2
+
3
+ const OrderedListToButtonLogicHook = require('../../src/scripting/logichook/logichooks/OrderedListToButtonLogicHook')
4
+
5
+ describe('logichooks.orderedListToButton', function () {
6
+ it('should convert ordered list to buttons', async function () {
7
+ const orderedListToButtonLogicHook = new OrderedListToButtonLogicHook()
8
+ const botMsg = {
9
+ messageText: `0. sometext.
10
+ 1.
11
+ 2.
12
+ 3.sdfdsf
13
+ 4. 3 days per week
14
+ 5.2.2
15
+ 6. 2.dfsdf
16
+ 7777.
17
+ 88euro
18
+ `,
19
+
20
+ buttons: [{ payload: 'existingButtonPayload', text: 'existingButtonText' }]
21
+ }
22
+
23
+ orderedListToButtonLogicHook.onBotPrepare({ botMsg })
24
+ assert.equal(botMsg.buttons?.length, 9)
25
+ assert.deepEqual(botMsg.buttons[0], { payload: 'existingButtonPayload', text: 'existingButtonText' })
26
+ assert.deepEqual(botMsg.buttons[1], { payload: '0', text: '0' })
27
+ assert.deepEqual(botMsg.buttons[2], { payload: '1', text: '1' })
28
+ assert.deepEqual(botMsg.buttons[3], { payload: '2', text: '2' })
29
+ assert.deepEqual(botMsg.buttons[4], { payload: '3', text: '3' })
30
+ assert.deepEqual(botMsg.buttons[5], { payload: '4', text: '4' })
31
+ assert.deepEqual(botMsg.buttons[6], { payload: '5', text: '5' })
32
+ assert.deepEqual(botMsg.buttons[7], { payload: '6', text: '6' })
33
+ assert.deepEqual(botMsg.buttons[8], { payload: '7777', text: '7777' })
34
+ })
35
+ })
@@ -0,0 +1,151 @@
1
+ const path = require('path')
2
+ const assert = require('chai').assert
3
+ const BotDriver = require('../../../index').BotDriver
4
+ const Capabilities = require('../../../index').Capabilities
5
+ const debug = require('debug')('botium-test-logichooks-waitforbot')
6
+ const util = require('util')
7
+
8
+ const createEchoConnector = () => ({ queueBotSays, caps }) => {
9
+ return {
10
+ UserSays (msg) {
11
+ const _send = (msg, timeout) => {
12
+ const botMsg = { sender: 'bot', sourceData: msg.sourceData, messageText: `You said ${msg}` }
13
+ if (msg.toLowerCase().indexOf('button') >= 0) {
14
+ botMsg.buttons = [{ text: 'button1' }, { text: 'button2' }]
15
+ }
16
+ if (msg.toLowerCase().indexOf('intent') >= 0) {
17
+ botMsg.nlp = { intent: { name: 'someIntent', confidence: 0.5 } }
18
+ }
19
+ setTimeout(() => {
20
+ debug(`${prefix} Connector is sending message ${util.inspect(botMsg)}`)
21
+ return queueBotSays(botMsg)
22
+ }, timeout)
23
+ }
24
+
25
+ const prefix = `Testcase "${caps[Capabilities.PROJECTNAME]}"`
26
+ debug(`${prefix} Connector got message ${util.inspect(msg)}`)
27
+ if (msg.messageText) {
28
+ msg.messageText.split('\n').forEach((msgPart, i) => {
29
+ if (msgPart) {
30
+ _send(msgPart, i * 50)
31
+ }
32
+ })
33
+ }
34
+ }
35
+ }
36
+ }
37
+ describe('scripting.asserters.convoStepParametersForAssert', function () {
38
+ beforeEach(async function () {
39
+ const myCaps = {
40
+ [Capabilities.PROJECTNAME]: 'asserters.text',
41
+ [Capabilities.CONTAINERMODE]: createEchoConnector(),
42
+ [Capabilities.WAITFORBOTTIMEOUT]: 1000,
43
+ [Capabilities.SCRIPTING_ENABLE_MULTIPLE_ASSERT_ERRORS]: true,
44
+ [Capabilities.SCRIPTING_CONVO_STEP_PARAMETERS]: '{"ignoreNotMatchedBotResponses": {"timeout": 200, "asserters": ["INTENT"]}}'
45
+ }
46
+ const driver = new BotDriver(myCaps)
47
+ this.compiler = driver.BuildCompiler()
48
+ this.container = await driver.Build()
49
+ })
50
+ afterEach(async function () {
51
+ await this.container.Stop()
52
+ await this.container.Clean()
53
+ })
54
+
55
+ describe('scripting.asserters.convoStepParametersForAssert.matchmode', function () {
56
+ it('should not accept bad chatbot response on exact match defined on step', async function () {
57
+ this.compiler.ReadScript(path.resolve(__dirname, 'convos'), 'convo_step_parameter_matchmode_failed.convo.txt')
58
+ assert.equal(this.compiler.convos.length, 1)
59
+
60
+ try {
61
+ await this.compiler.convos[0].Run(this.container)
62
+ assert.fail('should have failed')
63
+ } catch (err) {
64
+ assert.isTrue(err.message.indexOf('"You said Hello" expected to match "Hello"') >= 0)
65
+ }
66
+ })
67
+ it('should not accept bad chatbot response on exact match defined on step even with WER asserter', async function () {
68
+ this.compiler.ReadScript(path.resolve(__dirname, 'convos'), 'convo_step_parameter_matchmode_failed_wer.convo.txt')
69
+ assert.equal(this.compiler.convos.length, 1)
70
+
71
+ try {
72
+ await this.compiler.convos[0].Run(this.container)
73
+ assert.fail('should have failed')
74
+ } catch (err) {
75
+ assert.isTrue(err.message.indexOf('Word Error Rate (50%) higher than accepted (25%)') >= 0)
76
+ }
77
+ })
78
+ })
79
+
80
+ describe('scripting.asserters.convoStepParametersForAssert.retry', function () {
81
+ it('should retry until succesful main', async function () {
82
+ this.compiler.ReadScript(path.resolve(__dirname, 'convos'), 'convo_step_parameter_retry_main_good.convo.txt')
83
+ assert.equal(this.compiler.convos.length, 1)
84
+
85
+ await this.compiler.convos[0].Run(this.container)
86
+ })
87
+ it('should retry until succesful asserters', async function () {
88
+ this.compiler.ReadScript(path.resolve(__dirname, 'convos'), 'convo_step_parameter_retry_asserters_good.convo.txt')
89
+ assert.equal(this.compiler.convos.length, 1)
90
+
91
+ await this.compiler.convos[0].Run(this.container)
92
+ })
93
+ it('should retry until succesful asserters all', async function () {
94
+ this.compiler.ReadScript(path.resolve(__dirname, 'convos'), 'convo_step_parameter_retry_asserters_all_good.convo.txt')
95
+ assert.equal(this.compiler.convos.length, 1)
96
+
97
+ await this.compiler.convos[0].Run(this.container)
98
+ })
99
+ it('should retry until succesful main, configured in begin', async function () {
100
+ this.compiler.ReadScript(path.resolve(__dirname, 'convos'), 'convo_step_parameter_retry_main_good_begin.convo.txt')
101
+ assert.equal(this.compiler.convos.length, 1)
102
+
103
+ await this.compiler.convos[0].Run(this.container)
104
+ })
105
+ it('should retry until succesful asserters, configured by cap', async function () {
106
+ this.compiler.ReadScript(path.resolve(__dirname, 'convos'), 'convo_step_parameter_retry_asserters_good_global.convo.txt')
107
+ assert.equal(this.compiler.convos.length, 1)
108
+
109
+ await this.compiler.convos[0].Run(this.container)
110
+ })
111
+ it('should retry until timeout main', async function () {
112
+ this.compiler.ReadScript(path.resolve(__dirname, 'convos'), 'convo_step_parameter_retry_main_botium_timeout.convo.txt')
113
+ assert.equal(this.compiler.convos.length, 1)
114
+
115
+ try {
116
+ await this.compiler.convos[0].Run(this.container)
117
+ assert.fail('should have failed')
118
+ } catch (err) {
119
+ assert.isTrue(err.message.indexOf('error waiting for bot - Bot did not respond within 1s') >= 0)
120
+ }
121
+ })
122
+ it('should retry until timeout asserter', async function () {
123
+ this.compiler.ReadScript(path.resolve(__dirname, 'convos'), 'convo_step_parameter_retry_asserters_botium_timeout.convo.txt')
124
+ assert.equal(this.compiler.convos.length, 1)
125
+
126
+ try {
127
+ await this.compiler.convos[0].Run(this.container)
128
+ assert.fail('should have failed')
129
+ } catch (err) {
130
+ assert.isTrue(err.message.indexOf('error waiting for bot - Bot did not respond within 1s') >= 0)
131
+ }
132
+ })
133
+ it('should not retry on not retriable error', async function () {
134
+ this.compiler.ReadScript(path.resolve(__dirname, 'convos'), 'convo_step_parameter_retry_main_but_no_button.convo.txt')
135
+ assert.equal(this.compiler.convos.length, 1)
136
+
137
+ try {
138
+ await this.compiler.convos[0].Run(this.container)
139
+ assert.fail('should have failed')
140
+ } catch (err) {
141
+ assert.isTrue(err.message.indexOf('Expected button(s) with text "some not existing button"') >= 0)
142
+ }
143
+ })
144
+ it('should retry until every retriable is succesful', async function () {
145
+ this.compiler.ReadScript(path.resolve(__dirname, 'convos'), 'convo_step_parameter_retry_main_and_asserter.convo.txt')
146
+ assert.equal(this.compiler.convos.length, 1)
147
+
148
+ await this.compiler.convos[0].Run(this.container)
149
+ })
150
+ })
151
+ })
@@ -0,0 +1,6 @@
1
+ TEXT_GOOD
2
+ #me
3
+ Hello1
4
+
5
+ #bot
6
+ TEXT You said Hello1
@@ -0,0 +1,8 @@
1
+ convo_step_parameter_matchmode_failed
2
+
3
+ #me
4
+ Hello
5
+
6
+ #bot
7
+ Hello
8
+ CONVO_STEP_PARAMETERS {"matchingMode":"equals"}