botium-core 1.14.0 → 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 (38) hide show
  1. package/dist/botium-cjs.js +302 -94
  2. package/dist/botium-cjs.js.map +1 -1
  3. package/dist/botium-es.js +302 -94
  4. package/dist/botium-es.js.map +1 -1
  5. package/package.json +19 -19
  6. package/src/Capabilities.js +2 -0
  7. package/src/scripting/BotiumError.js +40 -3
  8. package/src/scripting/Convo.js +139 -28
  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/ConditionalBusinessHoursLogicHook.js +1 -1
  14. package/src/scripting/logichook/logichooks/ConditionalCapabilityValueBasedLogicHook.js +1 -1
  15. package/src/scripting/logichook/logichooks/ConditionalJsonPathBasedLogicHook.js +1 -1
  16. package/src/scripting/logichook/logichooks/ConditionalTimeBasedLogicHook.js +1 -1
  17. package/src/scripting/logichook/logichooks/ConvoStepParametersLogicHook.js +6 -0
  18. package/src/scripting/logichook/logichooks/OrderedListToButtonLogicHook.js +37 -0
  19. package/test/convo/fillAndApplyScriptingMemory.spec.js +11 -0
  20. package/test/logichooks/orderedListToButton.spec.js +35 -0
  21. package/test/scripting/asserters/convoStepParameters.spec.js +140 -0
  22. package/test/scripting/asserters/convos/TEXT_GOOD.convo.txt +6 -0
  23. package/test/scripting/asserters/convos/convo_step_parameter_matchmode_failed.convo.txt +8 -0
  24. package/test/scripting/asserters/convos/convo_step_parameter_retry_asserters_all_good.convo.txt +9 -0
  25. package/test/scripting/asserters/convos/convo_step_parameter_retry_asserters_botium_timeout.convo.txt +9 -0
  26. package/test/scripting/asserters/convos/convo_step_parameter_retry_asserters_good.convo.txt +9 -0
  27. package/test/scripting/asserters/convos/convo_step_parameter_retry_asserters_good_global.convo.txt +9 -0
  28. package/test/scripting/asserters/convos/convo_step_parameter_retry_main_and_asserter.convo.txt +10 -0
  29. package/test/scripting/asserters/convos/convo_step_parameter_retry_main_botium_timeout.convo.txt +9 -0
  30. package/test/scripting/asserters/convos/convo_step_parameter_retry_main_but_no_button.convo.txt +10 -0
  31. package/test/scripting/asserters/convos/convo_step_parameter_retry_main_good.convo.txt +9 -0
  32. package/test/scripting/asserters/convos/convo_step_parameter_retry_main_good_begin.convo.txt +11 -0
  33. package/test/scripting/logichooks/convos/conditional_steps_multiple_condition_groups_no_assertion.convo.txt +6 -6
  34. package/test/scripting/logichooks/convos/conditional_steps_multiple_mandatory_condition_groups.convo.txt +20 -0
  35. package/test/scripting/logichooks/convos/conditional_steps_multiple_optional_condition_groups.convo.txt +20 -0
  36. package/test/scripting/logichooks/customConditionalStepLogicHook.spec.js +16 -0
  37. package/test/scripting/matching/matchingmode.spec.js +4 -1
  38. 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.0",
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.22.15",
35
- "async": "^3.2.4",
34
+ "@babel/runtime": "^7.23.5",
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.0",
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.1",
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",
@@ -55,7 +55,7 @@
55
55
  "promise.allsettled": "^1.0.7",
56
56
  "randomatic": "^3.1.1",
57
57
  "request": "^2.88.2",
58
- "rimraf": "^5.0.1",
58
+ "rimraf": "^5.0.5",
59
59
  "sanitize-filename": "^1.6.3",
60
60
  "slugify": "^1.6.6",
61
61
  "socket.io": "^4.7.2",
@@ -63,31 +63,31 @@
63
63
  "socketio-auth": "^0.1.1",
64
64
  "swagger-jsdoc": "^6.2.8",
65
65
  "swagger-ui-express": "^5.0.0",
66
- "uuid": "^9.0.0",
66
+ "uuid": "^9.0.1",
67
67
  "word-error-rate": "0.0.7",
68
68
  "write-yaml": "^1.0.0",
69
69
  "xlsx": "^0.18.5",
70
70
  "xregexp": "^5.1.1",
71
- "yaml": "^2.3.2"
71
+ "yaml": "^2.3.4"
72
72
  },
73
73
  "devDependencies": {
74
- "@babel/core": "^7.22.17",
75
- "@babel/node": "^7.22.15",
76
- "@babel/plugin-transform-runtime": "^7.22.15",
77
- "@babel/preset-env": "^7.22.15",
78
- "chai": "^4.3.8",
74
+ "@babel/core": "^7.23.5",
75
+ "@babel/node": "^7.22.19",
76
+ "@babel/plugin-transform-runtime": "^7.23.4",
77
+ "@babel/preset-env": "^7.23.5",
78
+ "chai": "^4.3.10",
79
79
  "chai-as-promised": "^7.1.1",
80
80
  "cross-env": "^7.0.3",
81
- "eslint": "^8.49.0",
81
+ "eslint": "^8.55.0",
82
82
  "eslint-config-standard": "^17.1.0",
83
- "eslint-plugin-import": "^2.28.1",
84
- "eslint-plugin-mocha": "^10.1.0",
85
- "eslint-plugin-n": "^16.1.0",
83
+ "eslint-plugin-import": "^2.29.0",
84
+ "eslint-plugin-mocha": "^10.2.0",
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.3",
90
- "npm-check-updates": "^16.13.3",
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
 
@@ -275,8 +275,57 @@ class Convo {
275
275
  let botMsg = null
276
276
  let waitForBotSays = true
277
277
  let skipTranscriptStep = false
278
- for (let i = 0; i < this.conversation.length; i++) {
278
+ let conditionalGroupId = null
279
+ let conditionMetInGroup = false
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
279
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
+
280
329
  const currentStepIndex = i
281
330
  container.eventEmitter.emit(Events.CONVO_STEP_NEXT, container, convoStep, i)
282
331
  skipTranscriptStep = false
@@ -317,8 +366,8 @@ class Convo {
317
366
  const coreMsg = _.omit(removeBuffers(meMsg), ['sourceData'])
318
367
  debug(`${this.header.name}/${convoStep.stepTag}: user says (cleaned by binary and base64 data and sourceData) ${JSON.stringify(coreMsg, null, 2)}`)
319
368
  await new Promise(resolve => {
320
- if (container.caps.SIMULATE_WRITING_SPEED && meMsg.messageText && meMsg.messageText.length) {
321
- 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)
322
371
  } else {
323
372
  resolve()
324
373
  }
@@ -356,7 +405,6 @@ class Convo {
356
405
  throw failErr
357
406
  }
358
407
  } else if (convoStep.sender === 'bot') {
359
- const previousWaitForBotSays = waitForBotSays
360
408
  if (waitForBotSays) {
361
409
  botMsg = null
362
410
  } else {
@@ -406,22 +454,38 @@ class Convo {
406
454
  }
407
455
 
408
456
  if (convoStep.conditional) {
457
+ waitForBotSays = false
458
+ let endOfConditionalGroup = false
459
+ conditionalGroupId = convoStep.logicHooks.find(lh => lh.name.startsWith('CONDITIONAL_STEP')).args[1]
409
460
  const nextConvoStep = this.conversation[i + 1]
410
461
 
411
- if (!previousWaitForBotSays) {
412
- skipTranscriptStep = true
413
- }
414
- waitForBotSays = false
415
462
  if (!nextConvoStep || nextConvoStep.sender !== 'bot' || !nextConvoStep.logicHooks || !nextConvoStep.logicHooks.some(lh => lh.name.toUpperCase().startsWith('CONDITIONAL_STEP'))) {
416
- waitForBotSays = true
463
+ endOfConditionalGroup = true
417
464
  } else {
418
- const conditionalLogicHook = convoStep.logicHooks.find(lh => lh.name.startsWith('CONDITIONAL_STEP'))
419
465
  const nextConditionalLogicHook = nextConvoStep.logicHooks.find(lh => lh.name.startsWith('CONDITIONAL_STEP'))
420
- waitForBotSays = conditionalLogicHook.args[1] !== nextConditionalLogicHook.args[1]
466
+ endOfConditionalGroup = conditionalGroupId !== nextConditionalLogicHook.args[1]
421
467
  }
422
468
 
423
- if (convoStep.conditional.skip) {
469
+ if (convoStep.conditional.skip || conditionMetInGroup) {
470
+ skipTranscriptStep = true
471
+ if (endOfConditionalGroup && !conditionMetInGroup && !convoStep.optional) {
472
+ const failErr = new BotiumError(`${this.header.name}/${convoStep.stepTag}: Non of the conditions are met in ${conditionalGroupId ? `'${conditionalGroupId}' ` : ''}condition group`)
473
+ debug(failErr)
474
+ throw failErr
475
+ }
476
+ if (endOfConditionalGroup) {
477
+ waitForBotSays = !convoStep.optional
478
+ conditionalGroupId = undefined
479
+ conditionMetInGroup = false
480
+ }
424
481
  continue
482
+ } else {
483
+ conditionMetInGroup = true
484
+ if (endOfConditionalGroup) {
485
+ waitForBotSays = !convoStep.optional
486
+ conditionalGroupId = undefined
487
+ conditionMetInGroup = false
488
+ }
425
489
  }
426
490
  }
427
491
 
@@ -436,11 +500,32 @@ class Convo {
436
500
  }
437
501
  const isErrorHandledWithOptionConvoStep = (err) => {
438
502
  const nextConvoStep = this.conversation[i + 1]
503
+ const retryConfig = convoStepParameters?.ignoreNotMatchedBotResponses
504
+ const retryOn = convoStep.sender === 'bot' && retryConfig && retryConfig.timeout && retryConfig.mainAsserter
439
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
+ }
440
509
  waitForBotSays = false
441
510
  skipTranscriptStep = true
442
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
+ }
443
527
  }
528
+
444
529
  if (container.caps[Capabilities.SCRIPTING_ENABLE_MULTIPLE_ASSERT_ERRORS]) {
445
530
  assertErrors.push(err)
446
531
  return false
@@ -457,7 +542,7 @@ class Convo {
457
542
  const tomatch = this._resolveUtterancesToMatch(container, Object.assign({}, scriptingMemoryUpdate, scriptingMemory), messageText, botMsg)
458
543
  if (convoStep.not) {
459
544
  try {
460
- this.scriptingEvents.assertBotNotResponse(response, tomatch, `${this.header.name}/${convoStep.stepTag}`, lastMeConvoStep)
545
+ this.scriptingEvents.assertBotNotResponse(response, tomatch, `${this.header.name}/${convoStep.stepTag}`, lastMeConvoStep, convoStepParameters)
461
546
  } catch (err) {
462
547
  if (isErrorHandledWithOptionConvoStep(err)) {
463
548
  continue
@@ -465,7 +550,7 @@ class Convo {
465
550
  }
466
551
  } else {
467
552
  try {
468
- this.scriptingEvents.assertBotResponse(response, tomatch, `${this.header.name}/${convoStep.stepTag}`, lastMeConvoStep)
553
+ this.scriptingEvents.assertBotResponse(response, tomatch, `${this.header.name}/${convoStep.stepTag}`, lastMeConvoStep, convoStepParameters)
469
554
  } catch (err) {
470
555
  if (isErrorHandledWithOptionConvoStep(err)) {
471
556
  continue
@@ -474,7 +559,7 @@ class Convo {
474
559
  }
475
560
  } else if (convoStep.sourceData) {
476
561
  try {
477
- this._compareObject(container, scriptingMemory, convoStep, botMsg.sourceData, convoStep.sourceData, botMsg)
562
+ this._compareObject(container, scriptingMemory, convoStep, botMsg.sourceData, convoStep.sourceData, botMsg, convoStepParameters)
478
563
  } catch (err) {
479
564
  if (isErrorHandledWithOptionConvoStep(err)) {
480
565
  continue
@@ -492,20 +577,46 @@ class Convo {
492
577
  skipTranscriptStep = true
493
578
  continue
494
579
  }
495
- const failErr = botiumErrorFromErr(`${this.header.name}/${convoStep.stepTag}: assertion error - ${err.message || err}`, err)
496
- debug(failErr)
497
- try {
498
- this.scriptingEvents.fail && this.scriptingEvents.fail(failErr, lastMeConvoStep)
499
- } 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
500
592
  }
501
- if (container.caps[Capabilities.SCRIPTING_ENABLE_MULTIPLE_ASSERT_ERRORS] && err instanceof BotiumError) {
502
- 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
503
599
  } else {
504
- 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
+ }
505
614
  }
506
615
  }
507
616
  if (container.caps[Capabilities.SCRIPTING_ENABLE_MULTIPLE_ASSERT_ERRORS]) {
508
617
  if (assertErrors.length > 0) {
618
+ // this has no effect, but logically it has to be false
619
+ retryBotMessageDropBotResponse = false
509
620
  throw botiumErrorFromList(assertErrors, {})
510
621
  }
511
622
  } else {
@@ -563,7 +674,7 @@ class Convo {
563
674
  }
564
675
  }
565
676
 
566
- _compareObject (container, scriptingMemory, convoStep, result, expected, botMsg) {
677
+ _compareObject (container, scriptingMemory, convoStep, result, expected, botMsg, convoStepParameters) {
567
678
  if (expected === null || expected === undefined) return
568
679
 
569
680
  if (_.isArray(expected)) {
@@ -574,12 +685,12 @@ class Convo {
574
685
  throw new BotiumError(`${this.header.name}/${convoStep.stepTag}: bot response expected array length ${expected.length}, got ${result.length}`)
575
686
  }
576
687
  for (let i = 0; i < expected.length; i++) {
577
- this._compareObject(container, scriptingMemory, convoStep, result[i], expected[i])
688
+ this._compareObject(container, scriptingMemory, convoStep, result[i], expected[i], null, convoStepParameters)
578
689
  }
579
690
  } else if (_.isObject(expected)) {
580
691
  _.forOwn(expected, (value, key) => {
581
692
  if (Object.prototype.hasOwnProperty.call(result, key)) {
582
- this._compareObject(container, scriptingMemory, convoStep, result[key], expected[key])
693
+ this._compareObject(container, scriptingMemory, convoStep, result[key], expected[key], null, convoStepParameters)
583
694
  } else {
584
695
  throw new BotiumError(`${this.header.name}/${convoStep.stepTag}: bot response "${result}" missing expected property: ${key}`)
585
696
  }
@@ -588,7 +699,7 @@ class Convo {
588
699
  ScriptingMemory.fill(container, scriptingMemory, result, expected, this.scriptingEvents)
589
700
  const response = this._checkNormalizeText(container, result)
590
701
  const tomatch = this._resolveUtterancesToMatch(container, scriptingMemory, expected, botMsg)
591
- this.scriptingEvents.assertBotResponse(response, tomatch, `${this.header.name}/${convoStep.stepTag}`)
702
+ this.scriptingEvents.assertBotResponse(response, tomatch, `${this.header.name}/${convoStep.stepTag}`, null, convoStepParameters)
592
703
  }
593
704
  }
594
705
 
@@ -656,7 +767,7 @@ class Convo {
656
767
  }
657
768
 
658
769
  _checkBotRepliesConsumed (container) {
659
- if (container.caps.SCRIPTING_FORCE_BOT_CONSUMED) {
770
+ if (container.caps[Capabilities.SCRIPTING_FORCE_BOT_CONSUMED]) {
660
771
  const queueLength = container._QueueLength()
661
772
  if (queueLength === 1) {
662
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)