botium-core 1.14.1 → 1.14.3

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 (30) hide show
  1. package/dist/botium-cjs.js +268 -77
  2. package/dist/botium-cjs.js.map +1 -1
  3. package/dist/botium-es.js +268 -77
  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/Convo.js +113 -19
  9. package/src/scripting/ScriptingMemory.js +7 -0
  10. package/src/scripting/ScriptingProvider.js +79 -30
  11. package/src/scripting/logichook/LogicHookConsts.js +5 -2
  12. package/src/scripting/logichook/LogicHookUtils.js +8 -6
  13. package/src/scripting/logichook/logichooks/ConvoStepParametersLogicHook.js +6 -0
  14. package/src/scripting/logichook/logichooks/OrderedListToButtonLogicHook.js +37 -0
  15. package/test/convo/fillAndApplyScriptingMemory.spec.js +11 -0
  16. package/test/logichooks/orderedListToButton.spec.js +35 -0
  17. package/test/scripting/asserters/convoStepParameters.spec.js +140 -0
  18. package/test/scripting/asserters/convos/TEXT_GOOD.convo.txt +6 -0
  19. package/test/scripting/asserters/convos/convo_step_parameter_matchmode_failed.convo.txt +8 -0
  20. package/test/scripting/asserters/convos/convo_step_parameter_retry_asserters_all_good.convo.txt +9 -0
  21. package/test/scripting/asserters/convos/convo_step_parameter_retry_asserters_botium_timeout.convo.txt +9 -0
  22. package/test/scripting/asserters/convos/convo_step_parameter_retry_asserters_good.convo.txt +9 -0
  23. package/test/scripting/asserters/convos/convo_step_parameter_retry_asserters_good_global.convo.txt +9 -0
  24. package/test/scripting/asserters/convos/convo_step_parameter_retry_main_and_asserter.convo.txt +10 -0
  25. package/test/scripting/asserters/convos/convo_step_parameter_retry_main_botium_timeout.convo.txt +9 -0
  26. package/test/scripting/asserters/convos/convo_step_parameter_retry_main_but_no_button.convo.txt +10 -0
  27. package/test/scripting/asserters/convos/convo_step_parameter_retry_main_good.convo.txt +9 -0
  28. package/test/scripting/asserters/convos/convo_step_parameter_retry_main_good_begin.convo.txt +11 -0
  29. package/test/scripting/matching/matchingmode.spec.js +4 -1
  30. package/test/scripting/scriptingProvider.spec.js +38 -12
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "botium-core",
3
- "version": "1.14.1",
3
+ "version": "1.14.3",
4
4
  "description": "The Selenium for Chatbots",
5
5
  "main": "index.js",
6
6
  "module": "dist/botium-es.js",
@@ -31,12 +31,12 @@
31
31
  },
32
32
  "homepage": "https://www.botium.ai",
33
33
  "dependencies": {
34
- "@babel/runtime": "^7.23.2",
34
+ "@babel/runtime": "^7.23.5",
35
35
  "async": "^3.2.5",
36
36
  "body-parser": "^1.20.2",
37
37
  "boolean": "^3.2.0",
38
38
  "bottleneck": "^2.19.5",
39
- "csv-parse": "^5.5.2",
39
+ "csv-parse": "^5.5.3",
40
40
  "debug": "^4.3.4",
41
41
  "express": "^4.18.2",
42
42
  "globby": "11.0.4",
@@ -45,7 +45,7 @@
45
45
  "is-json": "^2.0.1",
46
46
  "jsonpath": "^1.1.1",
47
47
  "lodash": "^4.17.21",
48
- "markdown-it": "^13.0.2",
48
+ "markdown-it": "^14.0.0",
49
49
  "mime-types": "^2.1.35",
50
50
  "mkdirp": "^3.0.1",
51
51
  "moment": "^2.29.4",
@@ -71,23 +71,23 @@
71
71
  "yaml": "^2.3.4"
72
72
  },
73
73
  "devDependencies": {
74
- "@babel/core": "^7.23.3",
74
+ "@babel/core": "^7.23.5",
75
75
  "@babel/node": "^7.22.19",
76
- "@babel/plugin-transform-runtime": "^7.23.3",
77
- "@babel/preset-env": "^7.23.3",
76
+ "@babel/plugin-transform-runtime": "^7.23.4",
77
+ "@babel/preset-env": "^7.23.5",
78
78
  "chai": "^4.3.10",
79
79
  "chai-as-promised": "^7.1.1",
80
80
  "cross-env": "^7.0.3",
81
- "eslint": "^8.53.0",
81
+ "eslint": "^8.55.0",
82
82
  "eslint-config-standard": "^17.1.0",
83
83
  "eslint-plugin-import": "^2.29.0",
84
84
  "eslint-plugin-mocha": "^10.2.0",
85
- "eslint-plugin-n": "^16.3.1",
85
+ "eslint-plugin-n": "^16.4.0",
86
86
  "eslint-plugin-promise": "^6.1.1",
87
87
  "eslint-plugin-standard": "^4.1.0",
88
88
  "mocha": "^10.2.0",
89
- "nock": "^13.3.8",
90
- "npm-check-updates": "^16.14.6",
89
+ "nock": "^13.4.0",
90
+ "npm-check-updates": "^16.14.11",
91
91
  "nyc": "^15.1.0",
92
92
  "rollup": "2.79.1",
93
93
  "rollup-plugin-babel": "^4.4.0",
@@ -157,6 +157,8 @@ module.exports = {
157
157
  // varnames, testcasenames
158
158
  SCRIPTING_MEMORY_COLUMN_MODE: 'SCRIPTING_MEMORY_COLUMN_MODE',
159
159
  // Botium Lifecycle Hooks
160
+ SCRIPTING_CONVO_STEP_PARAMETERS: 'SCRIPTING_CONVO_STEP_PARAMETERS',
161
+ // Botium Lifecycle Hooks
160
162
  CUSTOMHOOK_ONBUILD: 'CUSTOMHOOK_ONBUILD',
161
163
  CUSTOMHOOK_ONSTART: 'CUSTOMHOOK_ONSTART',
162
164
  CUSTOMHOOK_ONUSERSAYS: 'CUSTOMHOOK_ONUSERSAYS',
@@ -90,6 +90,43 @@ const BotiumError = class BotiumError extends Error {
90
90
  return null
91
91
  }
92
92
  }
93
+
94
+ hasError ({ type, source }) {
95
+ if (this.context) {
96
+ const errArr = _.isArray(this.context) ? this.context : [this.context]
97
+ for (const err of errArr) {
98
+ if (err.type === 'list') {
99
+ for (const internal of err.errors) {
100
+ if ((!type || internal.type === type) && (!source || internal.source === source)) {
101
+ return true
102
+ }
103
+ }
104
+ }
105
+ if ((!type || err.type === type) && (!source || err.source === source)) {
106
+ return true
107
+ }
108
+ }
109
+ } else {
110
+ return false
111
+ }
112
+ }
113
+
114
+ toArray () {
115
+ if (this.context) {
116
+ let result = []
117
+ const errArr = _.isArray(this.context) ? this.context : [this.context]
118
+ for (const err of errArr) {
119
+ if (err.type === 'list') {
120
+ result = result.concat(err.errors)
121
+ } else {
122
+ result.push(err)
123
+ }
124
+ }
125
+ return result
126
+ } else {
127
+ return []
128
+ }
129
+ }
93
130
  }
94
131
 
95
132
  const _getChildErrorsFromContext = (context) => {
@@ -99,11 +136,11 @@ const _getChildErrorsFromContext = (context) => {
99
136
  return false
100
137
  }
101
138
 
102
- const botiumErrorFromErr = (message, err) => {
139
+ const botiumErrorFromErr = (message, err, context = {}) => {
103
140
  if (err instanceof BotiumError) {
104
- return new BotiumError(message, err.context, true)
141
+ return new BotiumError(message, { ...err.context, ...context }, true)
105
142
  } else {
106
- return new BotiumError(message, { err }, true)
143
+ return new BotiumError(message, { err, ...context }, true)
107
144
  }
108
145
  }
109
146
 
@@ -277,8 +277,55 @@ class Convo {
277
277
  let skipTranscriptStep = false
278
278
  let conditionalGroupId = null
279
279
  let conditionMetInGroup = false
280
- for (let i = 0; i < this.conversation.length; i++) {
280
+ let globalConvoStepParameters = container.caps[Capabilities.SCRIPTING_CONVO_STEP_PARAMETERS] || {}
281
+ let retryBotMessageTimeoutEnd = null
282
+ let retryBotMessageConvoId = null
283
+ let retryBotMessageDropBotResponse = false
284
+ for (let i = 0; i < this.conversation.length; i = (retryBotMessageDropBotResponse ? i : i + 1)) {
285
+ retryBotMessageDropBotResponse = false
281
286
  const convoStep = this.conversation[i]
287
+ const rawConvoStepParameters = convoStep.logicHooks.find(lh => lh.name === 'CONVO_STEP_PARAMETERS')?.args
288
+ let convoStepParameters = {}
289
+ if (rawConvoStepParameters && rawConvoStepParameters.length) {
290
+ let params
291
+ if (rawConvoStepParameters[0].trim().startsWith('{')) {
292
+ try {
293
+ params = JSON.parse(rawConvoStepParameters[0])
294
+ } catch (e) {
295
+ debug(`${this.header.name}/${convoStep.stepTag}: Failed to parse convo step parameters from JSON ${rawConvoStepParameters[0]}`)
296
+ }
297
+ }
298
+ if (!params || !Object.keys(params).length) {
299
+ params = {}
300
+ for (const param of rawConvoStepParameters) {
301
+ const semicolon = param.indexOf(':')
302
+ if (semicolon) {
303
+ try {
304
+ const name = param.substring(0, semicolon)
305
+ const value = param.substring(semicolon + 1)
306
+ params[name] = value
307
+ } catch (e) {
308
+ debug(`${this.header.name}/${convoStep.stepTag}: Failed to parse convo step parameter from arg ${param}`)
309
+ }
310
+ }
311
+ }
312
+ }
313
+
314
+ if (convoStep.sender === 'begin') {
315
+ globalConvoStepParameters = Object.assign({}, globalConvoStepParameters || {}, params)
316
+ } else {
317
+ convoStepParameters = Object.assign({}, globalConvoStepParameters || {}, params)
318
+ }
319
+ } else {
320
+ if (convoStep.sender !== 'begin') {
321
+ convoStepParameters = globalConvoStepParameters
322
+ }
323
+ }
324
+
325
+ if (Object.keys(convoStepParameters).length) {
326
+ debug(`${this.header.name}: using convo step parameters ${JSON.stringify(convoStepParameters)}`)
327
+ }
328
+
282
329
  const currentStepIndex = i
283
330
  container.eventEmitter.emit(Events.CONVO_STEP_NEXT, container, convoStep, i)
284
331
  skipTranscriptStep = false
@@ -319,8 +366,8 @@ class Convo {
319
366
  const coreMsg = _.omit(removeBuffers(meMsg), ['sourceData'])
320
367
  debug(`${this.header.name}/${convoStep.stepTag}: user says (cleaned by binary and base64 data and sourceData) ${JSON.stringify(coreMsg, null, 2)}`)
321
368
  await new Promise(resolve => {
322
- if (container.caps.SIMULATE_WRITING_SPEED && meMsg.messageText && meMsg.messageText.length) {
323
- setTimeout(() => resolve(), container.caps.SIMULATE_WRITING_SPEED * meMsg.messageText.length)
369
+ if (container.caps[Capabilities.SIMULATE_WRITING_SPEED] && meMsg.messageText && meMsg.messageText.length) {
370
+ setTimeout(() => resolve(), container.caps[Capabilities.SIMULATE_WRITING_SPEED] * meMsg.messageText.length)
324
371
  } else {
325
372
  resolve()
326
373
  }
@@ -453,11 +500,32 @@ class Convo {
453
500
  }
454
501
  const isErrorHandledWithOptionConvoStep = (err) => {
455
502
  const nextConvoStep = this.conversation[i + 1]
503
+ const retryConfig = convoStepParameters?.ignoreNotMatchedBotResponses
504
+ const retryOn = convoStep.sender === 'bot' && retryConfig && retryConfig.timeout && retryConfig.mainAsserter
456
505
  if (convoStep.optional && nextConvoStep && nextConvoStep.sender === 'bot') {
506
+ if (retryOn) {
507
+ debug(`${this.header.name}/${convoStep.stepTag}: Retry failed asserter is ignored on optional convo`)
508
+ }
457
509
  waitForBotSays = false
458
510
  skipTranscriptStep = true
459
511
  return true
512
+ } else if (retryOn) {
513
+ if (!retryBotMessageTimeoutEnd || retryBotMessageConvoId !== convoStep.stepTag) {
514
+ retryBotMessageTimeoutEnd = transcriptStep.stepBegin.getTime() + +retryConfig.timeout
515
+ retryBotMessageConvoId = convoStep.stepTag
516
+ }
517
+
518
+ const now = new Date().getTime()
519
+ const timeoutRemaining = retryBotMessageTimeoutEnd - now
520
+ if (timeoutRemaining > 0) {
521
+ debug(`${this.header.name}/${convoStep.stepTag}: Convo step retry on, timeout remaining: ${timeoutRemaining}, error: "${err.message}"`)
522
+ retryBotMessageDropBotResponse = true
523
+ return false
524
+ } else {
525
+ debug(`${this.header.name}/${convoStep.stepTag}: Convo step retry on, but timeout is over. error: "${err.message}"`)
526
+ }
460
527
  }
528
+
461
529
  if (container.caps[Capabilities.SCRIPTING_ENABLE_MULTIPLE_ASSERT_ERRORS]) {
462
530
  assertErrors.push(err)
463
531
  return false
@@ -474,7 +542,7 @@ class Convo {
474
542
  const tomatch = this._resolveUtterancesToMatch(container, Object.assign({}, scriptingMemoryUpdate, scriptingMemory), messageText, botMsg)
475
543
  if (convoStep.not) {
476
544
  try {
477
- this.scriptingEvents.assertBotNotResponse(response, tomatch, `${this.header.name}/${convoStep.stepTag}`, lastMeConvoStep)
545
+ this.scriptingEvents.assertBotNotResponse(response, tomatch, `${this.header.name}/${convoStep.stepTag}`, lastMeConvoStep, convoStepParameters)
478
546
  } catch (err) {
479
547
  if (isErrorHandledWithOptionConvoStep(err)) {
480
548
  continue
@@ -482,7 +550,7 @@ class Convo {
482
550
  }
483
551
  } else {
484
552
  try {
485
- this.scriptingEvents.assertBotResponse(response, tomatch, `${this.header.name}/${convoStep.stepTag}`, lastMeConvoStep)
553
+ this.scriptingEvents.assertBotResponse(response, tomatch, `${this.header.name}/${convoStep.stepTag}`, lastMeConvoStep, convoStepParameters)
486
554
  } catch (err) {
487
555
  if (isErrorHandledWithOptionConvoStep(err)) {
488
556
  continue
@@ -491,7 +559,7 @@ class Convo {
491
559
  }
492
560
  } else if (convoStep.sourceData) {
493
561
  try {
494
- this._compareObject(container, scriptingMemory, convoStep, botMsg.sourceData, convoStep.sourceData, botMsg)
562
+ this._compareObject(container, scriptingMemory, convoStep, botMsg.sourceData, convoStep.sourceData, botMsg, convoStepParameters)
495
563
  } catch (err) {
496
564
  if (isErrorHandledWithOptionConvoStep(err)) {
497
565
  continue
@@ -509,20 +577,46 @@ class Convo {
509
577
  skipTranscriptStep = true
510
578
  continue
511
579
  }
512
- const failErr = botiumErrorFromErr(`${this.header.name}/${convoStep.stepTag}: assertion error - ${err.message || err}`, err)
513
- debug(failErr)
514
- try {
515
- this.scriptingEvents.fail && this.scriptingEvents.fail(failErr, lastMeConvoStep)
516
- } catch (failErr) {
580
+
581
+ const errors = err.toArray ? err.toArray() : []
582
+ const retryConfig = convoStepParameters?.ignoreNotMatchedBotResponses
583
+ const retryOn =
584
+ convoStep.sender === 'bot' &&
585
+ retryConfig &&
586
+ retryConfig.timeout &&
587
+ errors.length &&
588
+ errors.filter(({ type, source, asserter }) => type === 'asserter' && (retryConfig.allAsserters || (retryConfig.asserters && retryConfig.asserters.includes(asserter)))).length
589
+ if (retryOn && (!retryBotMessageTimeoutEnd || retryBotMessageConvoId !== convoStep.stepTag)) {
590
+ retryBotMessageTimeoutEnd = transcriptStep.stepBegin.getTime() + +retryConfig.timeout
591
+ retryBotMessageConvoId = convoStep.stepTag
517
592
  }
518
- if (container.caps[Capabilities.SCRIPTING_ENABLE_MULTIPLE_ASSERT_ERRORS] && err instanceof BotiumError) {
519
- assertErrors.push(err)
593
+
594
+ const now = new Date().getTime()
595
+ const timeoutRemaining = retryOn && (retryBotMessageTimeoutEnd - now)
596
+ if (retryOn && timeoutRemaining > 0) {
597
+ debug(`${this.header.name}/${convoStep.stepTag}: Convo step retry on, timeout remaining: ${timeoutRemaining}, error: "${err.message}"`)
598
+ retryBotMessageDropBotResponse = true
520
599
  } else {
521
- throw failErr
600
+ if (retryOn && timeoutRemaining <= 0) {
601
+ debug(`${this.header.name}/${convoStep.stepTag}: Convo step retry on, but timeout is over. error: "${err.message}"`)
602
+ }
603
+ const failErr = botiumErrorFromErr(`${this.header.name}/${convoStep.stepTag}: assertion error - ${err.message || err}`, err)
604
+ debug(failErr)
605
+ try {
606
+ this.scriptingEvents.fail && this.scriptingEvents.fail(failErr, lastMeConvoStep)
607
+ } catch (failErr) {
608
+ }
609
+ if (container.caps[Capabilities.SCRIPTING_ENABLE_MULTIPLE_ASSERT_ERRORS] && err instanceof BotiumError) {
610
+ assertErrors.push(err)
611
+ } else {
612
+ throw failErr
613
+ }
522
614
  }
523
615
  }
524
616
  if (container.caps[Capabilities.SCRIPTING_ENABLE_MULTIPLE_ASSERT_ERRORS]) {
525
617
  if (assertErrors.length > 0) {
618
+ // this has no effect, but logically it has to be false
619
+ retryBotMessageDropBotResponse = false
526
620
  throw botiumErrorFromList(assertErrors, {})
527
621
  }
528
622
  } else {
@@ -580,7 +674,7 @@ class Convo {
580
674
  }
581
675
  }
582
676
 
583
- _compareObject (container, scriptingMemory, convoStep, result, expected, botMsg) {
677
+ _compareObject (container, scriptingMemory, convoStep, result, expected, botMsg, convoStepParameters) {
584
678
  if (expected === null || expected === undefined) return
585
679
 
586
680
  if (_.isArray(expected)) {
@@ -591,12 +685,12 @@ class Convo {
591
685
  throw new BotiumError(`${this.header.name}/${convoStep.stepTag}: bot response expected array length ${expected.length}, got ${result.length}`)
592
686
  }
593
687
  for (let i = 0; i < expected.length; i++) {
594
- this._compareObject(container, scriptingMemory, convoStep, result[i], expected[i])
688
+ this._compareObject(container, scriptingMemory, convoStep, result[i], expected[i], null, convoStepParameters)
595
689
  }
596
690
  } else if (_.isObject(expected)) {
597
691
  _.forOwn(expected, (value, key) => {
598
692
  if (Object.prototype.hasOwnProperty.call(result, key)) {
599
- this._compareObject(container, scriptingMemory, convoStep, result[key], expected[key])
693
+ this._compareObject(container, scriptingMemory, convoStep, result[key], expected[key], null, convoStepParameters)
600
694
  } else {
601
695
  throw new BotiumError(`${this.header.name}/${convoStep.stepTag}: bot response "${result}" missing expected property: ${key}`)
602
696
  }
@@ -605,7 +699,7 @@ class Convo {
605
699
  ScriptingMemory.fill(container, scriptingMemory, result, expected, this.scriptingEvents)
606
700
  const response = this._checkNormalizeText(container, result)
607
701
  const tomatch = this._resolveUtterancesToMatch(container, scriptingMemory, expected, botMsg)
608
- this.scriptingEvents.assertBotResponse(response, tomatch, `${this.header.name}/${convoStep.stepTag}`)
702
+ this.scriptingEvents.assertBotResponse(response, tomatch, `${this.header.name}/${convoStep.stepTag}`, null, convoStepParameters)
609
703
  }
610
704
  }
611
705
 
@@ -673,7 +767,7 @@ class Convo {
673
767
  }
674
768
 
675
769
  _checkBotRepliesConsumed (container) {
676
- if (container.caps.SCRIPTING_FORCE_BOT_CONSUMED) {
770
+ if (container.caps[Capabilities.SCRIPTING_FORCE_BOT_CONSUMED]) {
677
771
  const queueLength = container._QueueLength()
678
772
  if (queueLength === 1) {
679
773
  throw new Error('There is an unread bot reply in queue')
@@ -261,6 +261,13 @@ const _apply = (scriptingMemory, str, caps, mockMsg) => {
261
261
  return arg
262
262
  }
263
263
  })
264
+ args = args.map(arg => {
265
+ const argStr = `${arg}`
266
+ if (argStr.startsWith('$')) {
267
+ return scriptingMemory[argStr.substring(1)] || arg
268
+ }
269
+ return arg
270
+ })
264
271
  str = str.replace(match, SCRIPTING_FUNCTIONS[key].handler(caps, ...args, mockMsg))
265
272
  } else {
266
273
  str = str.replace(match, SCRIPTING_FUNCTIONS[key].handler(caps))
@@ -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,12 +130,13 @@ 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]))
139
140
  const asserterType = this.caps[Capabilities.SCRIPTING_MATCHING_MODE] === 'wer' ? 'Word Error Rate Asserter' : 'Text Match Asserter'
140
141
  if (_.isNil(found)) {
141
142
  if (this.caps[Capabilities.SCRIPTING_MATCHING_MODE] === 'wer') {
@@ -191,12 +192,13 @@ module.exports = class ScriptingProvider {
191
192
  }
192
193
  }
193
194
  },
194
- assertBotNotResponse: (botresponse, nottomatch, stepTag, meMsg) => {
195
+ assertBotNotResponse: (botresponse, nottomatch, stepTag, meMsg, convoStepParameters) => {
195
196
  if (!_.isArray(nottomatch)) {
196
197
  nottomatch = [nottomatch]
197
198
  }
198
199
  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]))
200
+ const matchFn = convoStepParameters.matchingMode ? (getMatchFunction(convoStepParameters.matchingMode) || this.matchFn) : this.matchFn
201
+ const found = _.find(nottomatch, (utt) => matchFn(botresponse, utt, this.caps[Capabilities.SCRIPTING_MATCHING_MODE_ARGS]))
200
202
  const asserterType = this.caps[Capabilities.SCRIPTING_MATCHING_MODE] === 'wer' ? 'Word Error Rate Asserter' : 'Text Match Asserter'
201
203
  if (!_.isNil(found)) {
202
204
  if (this.caps[Capabilities.SCRIPTING_MATCHING_MODE] === 'wer') {
@@ -270,6 +272,22 @@ module.exports = class ScriptingProvider {
270
272
  assertConvoStep: 'assertNotConvoStep',
271
273
  assertConvoEnd: 'assertNotConvoEnd'
272
274
  }
275
+ const updateExceptionContext = (promise, asserter) => {
276
+ const updateError = (err) => {
277
+ if (err instanceof BotiumError) {
278
+ if (!err.context) {
279
+ err.context = {}
280
+ }
281
+
282
+ err.context.asserter = asserter.name
283
+
284
+ throw err
285
+ } else {
286
+ throw botiumErrorFromErr(_.isString(err) ? err : err.message, err, { asserter: asserter.name })
287
+ }
288
+ }
289
+ return promise.catch(err => updateError(err))
290
+ }
273
291
  const callAsserter = (asserterSpec, asserter, params) => {
274
292
  if (asserterSpec.not) {
275
293
  const notAsserterType = mapNot[asserterType]
@@ -301,18 +319,35 @@ module.exports = class ScriptingProvider {
301
319
 
302
320
  const convoAsserter = asserters
303
321
  .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
322
+ .map(a => ({
323
+ asserter: a,
324
+ promise: callAsserter(a, this.asserters[a.name], {
325
+ convo,
326
+ convoStep,
327
+ scriptingMemory,
328
+ container,
329
+ args: ScriptingMemory.applyToArgs(a.args, scriptingMemory, container.caps, rest.botMsg),
330
+ isGlobal: false,
331
+ ...rest
332
+ })
312
333
  }))
334
+ .map(({ promise, asserter }) => updateExceptionContext(promise, asserter))
335
+
313
336
  const globalAsserter = Object.values(this.globalAsserter)
314
337
  .filter(a => a[asserterType])
315
- .map(a => p(this.retryHelperAsserter, () => a[asserterType]({ convo, convoStep, scriptingMemory, container, args: [], isGlobal: true, ...rest })))
338
+ .map(a => ({
339
+ asserter: a,
340
+ promise: p(this.retryHelperAsserter, () => a[asserterType]({
341
+ convo,
342
+ convoStep,
343
+ scriptingMemory,
344
+ container,
345
+ args: [],
346
+ isGlobal: true,
347
+ ...rest
348
+ }))
349
+ }))
350
+ .map(({ promise, asserter }) => updateExceptionContext(promise, asserter))
316
351
 
317
352
  const allPromises = [...convoAsserter, ...globalAsserter]
318
353
  if (this.caps[Capabilities.SCRIPTING_ENABLE_MULTIPLE_ASSERT_ERRORS]) {
@@ -335,8 +370,10 @@ module.exports = class ScriptingProvider {
335
370
  throw Error(`Unknown hookType ${hookType}`)
336
371
  }
337
372
 
338
- const convoStepPromises = (logicHooks || [])
373
+ const localHooks = (logicHooks || [])
339
374
  .filter(l => this.logicHooks[l.name][hookType])
375
+
376
+ const convoStepPromises = localHooks
340
377
  .map(l => p(this.retryHelperLogicHook, () => this.logicHooks[l.name][hookType]({
341
378
  convo,
342
379
  convoStep,
@@ -347,17 +384,24 @@ module.exports = class ScriptingProvider {
347
384
  ...rest
348
385
  })))
349
386
 
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 })))
387
+ const globalHooks = Object.values(this.globalLogicHook).filter(l => l[hookType])
388
+ const globalPromises = globalHooks.map(l => p(this.retryHelperLogicHook, () => l[hookType]({ convo, convoStep, scriptingMemory, container, args: [], isGlobal: true, ...rest })))
353
389
 
354
390
  const allPromises = [...convoStepPromises, ...globalPromises]
355
- if (allPromises.length > 0) return Promise.all(allPromises).then(() => true)
391
+
392
+ if (allPromises.length > 0) {
393
+ return Promise.all(allPromises).then(() => {
394
+ return {
395
+ // just returning some humanreadable
396
+ hooks: [...localHooks, ...globalHooks].map(h => h.name || h.context?.ref || JSON.stringify(h))
397
+ }
398
+ })
399
+ }
356
400
  return Promise.resolve(false)
357
401
  }
358
402
 
359
403
  _createUserInputPromises ({ convo, convoStep, scriptingMemory, container, ...rest }) {
360
- const convoStepPromises = (convoStep.userInputs || [])
404
+ const convoStepPromises = (convoStep?.userInputs || [])
361
405
  .filter(ui => this.userInputs[ui.name])
362
406
  .map(ui => p(this.retryHelperUserInput, () => this.userInputs[ui.name].setUserInput({
363
407
  convo,
@@ -424,6 +468,11 @@ module.exports = class ScriptingProvider {
424
468
  }
425
469
  }
426
470
 
471
+ // Livechat, and crawler using logichooks too. So they need script context
472
+ BuildScriptContext () {
473
+ return this._buildScriptContext()
474
+ }
475
+
427
476
  Build () {
428
477
  const CompilerXlsx = require('./CompilerXlsx')
429
478
  this.compilers[Constants.SCRIPTING_FORMAT_XSLX] = new CompilerXlsx(this._buildScriptContext(), this.caps)
@@ -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
+ }