botium-core 1.14.9 → 1.15.2

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.
@@ -277,6 +277,10 @@ class Convo {
277
277
  let skipTranscriptStep = false
278
278
  let conditionalGroupId = null
279
279
  let conditionMetInGroup = false
280
+ let skipOptionalStep = false
281
+ // If there are optional step(s) in the conversation, and the message from the bot fails on each optional bot step(s) and/or mandatory bot step, then we have an unexpected message.
282
+ // So in this case an unexpected error should be shown instead of the latest assertion error.
283
+ let optionalStepAssertionError = false
280
284
  let globalConvoStepParameters = container.caps[Capabilities.SCRIPTING_CONVO_STEP_PARAMETERS] || {}
281
285
  let retryBotMessageTimeoutEnd = null
282
286
  let retryBotMessageConvoId = null
@@ -284,6 +288,13 @@ class Convo {
284
288
  for (let i = 0; i < this.conversation.length; i = (retryBotMessageDropBotResponse ? i : i + 1)) {
285
289
  retryBotMessageDropBotResponse = false
286
290
  const convoStep = this.conversation[i]
291
+ if (!convoStep.optional) {
292
+ skipOptionalStep = false
293
+ }
294
+ if (convoStep.optional && skipOptionalStep) {
295
+ // If there are multiple optional steps, and the previous optional step was timeout, then the next optional step should be skipped to prevent too long convo run with multiple timeout.
296
+ continue
297
+ }
287
298
  const rawConvoStepParameters = convoStep.logicHooks.find(lh => lh.name === 'CONVO_STEP_PARAMETERS')?.args
288
299
  let convoStepParameters = {}
289
300
  if (rawConvoStepParameters && rawConvoStepParameters.length) {
@@ -426,7 +437,8 @@ class Convo {
426
437
  } catch (err) {
427
438
  transcriptStep.botEnd = new Date()
428
439
 
429
- if (convoStep.optional) {
440
+ if (!(err.message.indexOf('Bot did not respond within') < 0) && convoStep.optional) {
441
+ skipOptionalStep = true
430
442
  continue
431
443
  }
432
444
 
@@ -512,6 +524,7 @@ class Convo {
512
524
  }
513
525
  waitForBotSays = false
514
526
  skipTranscriptStep = true
527
+ optionalStepAssertionError = true
515
528
  return true
516
529
  } else if (retryOn) {
517
530
  if (!retryBotMessageTimeoutEnd || retryBotMessageConvoId !== convoStep.stepTag) {
@@ -531,9 +544,18 @@ class Convo {
531
544
  }
532
545
 
533
546
  if (container.caps[Capabilities.SCRIPTING_ENABLE_MULTIPLE_ASSERT_ERRORS]) {
534
- assertErrors.push(err)
547
+ if (optionalStepAssertionError) {
548
+ optionalStepAssertionError = false
549
+ assertErrors.push(new BotiumError(`${this.header.name}: Unexpected message.`))
550
+ } else {
551
+ assertErrors.push(err)
552
+ }
535
553
  return false
536
554
  } else {
555
+ if (optionalStepAssertionError) {
556
+ optionalStepAssertionError = false
557
+ throw new BotiumError(`${this.header.name}: Unexpected message.`)
558
+ }
537
559
  throw err
538
560
  }
539
561
  }
@@ -547,6 +569,7 @@ class Convo {
547
569
  if (convoStep.not) {
548
570
  try {
549
571
  this.scriptingEvents.assertBotNotResponse(response, tomatch, `${this.header.name}/${convoStep.stepTag}`, lastMeConvoStep, convoStepParameters)
572
+ optionalStepAssertionError = false
550
573
  } catch (err) {
551
574
  if (isErrorHandledWithOptionConvoStep(err)) {
552
575
  continue
@@ -555,6 +578,7 @@ class Convo {
555
578
  } else {
556
579
  try {
557
580
  this.scriptingEvents.assertBotResponse(response, tomatch, `${this.header.name}/${convoStep.stepTag}`, lastMeConvoStep, convoStepParameters)
581
+ optionalStepAssertionError = false
558
582
  } catch (err) {
559
583
  if (isErrorHandledWithOptionConvoStep(err)) {
560
584
  continue
@@ -564,6 +588,7 @@ class Convo {
564
588
  } else if (convoStep.sourceData) {
565
589
  try {
566
590
  this._compareObject(container, scriptingMemory, convoStep, botMsg.sourceData, convoStep.sourceData, botMsg, convoStepParameters)
591
+ optionalStepAssertionError = false
567
592
  } catch (err) {
568
593
  if (isErrorHandledWithOptionConvoStep(err)) {
569
594
  continue
@@ -574,11 +599,13 @@ class Convo {
574
599
  try {
575
600
  await this.scriptingEvents.assertConvoStep({ convo: this, convoStep, container, scriptingMemory, botMsg, transcript, transcriptStep })
576
601
  await this.scriptingEvents.onBotEnd({ convo: this, convoStep, container, scriptingMemory, botMsg, transcript, transcriptStep })
602
+ optionalStepAssertionError = false
577
603
  } catch (err) {
578
604
  const nextConvoStep = this.conversation[i + 1]
579
605
  if (convoStep.optional && nextConvoStep && nextConvoStep.sender === 'bot') {
580
606
  waitForBotSays = false
581
607
  skipTranscriptStep = true
608
+ optionalStepAssertionError = true
582
609
  continue
583
610
  }
584
611
 
@@ -611,8 +638,17 @@ class Convo {
611
638
  } catch (failErr) {
612
639
  }
613
640
  if (container.caps[Capabilities.SCRIPTING_ENABLE_MULTIPLE_ASSERT_ERRORS] && err instanceof BotiumError) {
614
- assertErrors.push(err)
641
+ if (optionalStepAssertionError) {
642
+ optionalStepAssertionError = false
643
+ assertErrors.push(new BotiumError(`${this.header.name}: Unexpected message.`))
644
+ } else {
645
+ assertErrors.push(err)
646
+ }
615
647
  } else {
648
+ if (optionalStepAssertionError) {
649
+ optionalStepAssertionError = false
650
+ throw new BotiumError(`${this.header.name}: Unexpected message.`)
651
+ }
616
652
  throw failErr
617
653
  }
618
654
  }
@@ -789,7 +825,7 @@ class Convo {
789
825
  }
790
826
 
791
827
  _checkNormalizeText (container, str) {
792
- return normalizeText(str, !!container.caps[Capabilities.SCRIPTING_NORMALIZE_TEXT])
828
+ return normalizeText(str, container.caps)
793
829
  }
794
830
 
795
831
  expandPartialConvos () {
@@ -83,9 +83,9 @@ module.exports = class ScriptingProvider {
83
83
  this.utterances = {}
84
84
  this.matchFn = null
85
85
  this.asserters = {}
86
- this.globalAsserter = {}
86
+ this.globalAsserters = {}
87
87
  this.logicHooks = {}
88
- this.globalLogicHook = {}
88
+ this.globalLogicHooks = {}
89
89
  this.userInputs = {}
90
90
  this.partialConvos = {}
91
91
  this.scriptingMemories = []
@@ -319,8 +319,9 @@ module.exports = class ScriptingProvider {
319
319
  }
320
320
  }
321
321
 
322
- const convoAsserter = asserters
323
- .filter(a => this.asserters[a.name][asserterType])
322
+ const localAsserters = (asserters || []).filter(a => this.asserters[a.name][asserterType])
323
+
324
+ const convoStepPromises = localAsserters
324
325
  .map(a => ({
325
326
  asserter: a,
326
327
  promise: callAsserter(a, this.asserters[a.name], {
@@ -335,8 +336,12 @@ module.exports = class ScriptingProvider {
335
336
  }))
336
337
  .map(({ promise, asserter }) => updateExceptionContext(promise, asserter))
337
338
 
338
- const globalAsserter = Object.values(this.globalAsserter)
339
+ const globalAsserters = Object.keys(this.globalAsserters)
340
+ .filter(name => localAsserters.map(a => a.name).indexOf(name) < 0)
341
+ .reduce((agg, name) => [...agg, this.globalAsserters[name]], [])
339
342
  .filter(a => a[asserterType])
343
+
344
+ const globalPromises = globalAsserters
340
345
  .map(a => ({
341
346
  asserter: a,
342
347
  promise: p(this.retryHelperAsserter, () => a[asserterType]({
@@ -351,7 +356,7 @@ module.exports = class ScriptingProvider {
351
356
  }))
352
357
  .map(({ promise, asserter }) => updateExceptionContext(promise, asserter))
353
358
 
354
- const allPromises = [...convoAsserter, ...globalAsserter]
359
+ const allPromises = [...convoStepPromises, ...globalPromises]
355
360
  if (this.caps[Capabilities.SCRIPTING_ENABLE_MULTIPLE_ASSERT_ERRORS]) {
356
361
  return Promise.allSettled(allPromises).then((results) => {
357
362
  const rejected = results.filter(result => result.status === 'rejected').map(result => result.reason)
@@ -372,8 +377,7 @@ module.exports = class ScriptingProvider {
372
377
  throw Error(`Unknown hookType ${hookType}`)
373
378
  }
374
379
 
375
- const localHooks = (logicHooks || [])
376
- .filter(l => this.logicHooks[l.name][hookType])
380
+ const localHooks = (logicHooks || []).filter(l => this.logicHooks[l.name][hookType])
377
381
 
378
382
  const convoStepPromises = localHooks
379
383
  .map(l => p(this.retryHelperLogicHook, () => this.logicHooks[l.name][hookType]({
@@ -386,7 +390,10 @@ module.exports = class ScriptingProvider {
386
390
  ...rest
387
391
  })))
388
392
 
389
- const globalHooks = Object.values(this.globalLogicHook).filter(l => l[hookType])
393
+ const globalHooks = Object.keys(this.globalLogicHooks)
394
+ .filter(name => localHooks.map(l => l.name).indexOf(name) < 0)
395
+ .reduce((agg, name) => [...agg, this.globalLogicHooks[name]], [])
396
+ .filter(l => l[hookType])
390
397
  const globalPromises = globalHooks.map(l => p(this.retryHelperLogicHook, () => l[hookType]({ convo, convoStep, scriptingMemory, container, args: [], isGlobal: true, ...rest })))
391
398
 
392
399
  const allPromises = [...convoStepPromises, ...globalPromises]
@@ -499,9 +506,9 @@ module.exports = class ScriptingProvider {
499
506
 
500
507
  const logicHookUtils = new LogicHookUtils({ buildScriptContext: this._buildScriptContext(), caps: this.caps })
501
508
  this.asserters = logicHookUtils.asserters
502
- this.globalAsserter = logicHookUtils.getGlobalAsserter()
509
+ this.globalAsserters = logicHookUtils.getGlobalAsserters()
503
510
  this.logicHooks = logicHookUtils.logicHooks
504
- this.globalLogicHook = logicHookUtils.getGlobalLogicHook()
511
+ this.globalLogicHooks = logicHookUtils.getGlobalLogicHooks()
505
512
  this.userInputs = logicHookUtils.userInputs
506
513
  }
507
514
 
@@ -4,9 +4,35 @@ 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 Capabilities = require('../Capabilities')
7
8
  const WHITE_SPACES_EXCEPT_SPACE_CHAR_AT_THE_END = /[\n\t\r]+$/
8
9
 
9
- const normalizeText = (str, doCleanup) => {
10
+ const normalizeText = (str, doCleanupOrCaps) => {
11
+ // TODO testlog
12
+ debug('yxc1', doCleanupOrCaps)
13
+ let basic
14
+ let charactersRemove = false
15
+ let regexpRemove = false
16
+ if (_.isBoolean(doCleanupOrCaps) || _.isNil(doCleanupOrCaps)) {
17
+ debug('Normalize text: backward compatibility mode. Use caps instead of boolean flag')
18
+ basic = !!doCleanupOrCaps
19
+ } else {
20
+ const caps = doCleanupOrCaps
21
+ basic = !!caps[Capabilities.SCRIPTING_NORMALIZE_TEXT]
22
+ if (caps[Capabilities.SCRIPTING_NORMALIZE_TEXT_REMOVE_CHARACTERES]) {
23
+ charactersRemove = caps[Capabilities.SCRIPTING_NORMALIZE_TEXT_REMOVE_CHARACTERES]
24
+ if (_.isString(charactersRemove)) {
25
+ const splitted = charactersRemove.split(/(?<!\/),/).map(e => e.trim()).map(e => e.split('/,').join(',').split('//').join('/')).filter(c => c.length > 0)
26
+ charactersRemove = splitted.length ? splitted : [charactersRemove]
27
+ } else if (!_.isArray(charactersRemove)) {
28
+ charactersRemove = false
29
+ }
30
+ }
31
+ if (caps[Capabilities.SCRIPTING_NORMALIZE_TEXT_REMOVE_REGEXP]) {
32
+ regexpRemove = new RegExp(caps[Capabilities.SCRIPTING_NORMALIZE_TEXT_REMOVE_REGEXP], 'ug')
33
+ }
34
+ }
35
+
10
36
  if (str && _.isArray(str)) {
11
37
  str = str.join(' ')
12
38
  } else if (str && !_.isString(str)) {
@@ -16,27 +42,38 @@ const normalizeText = (str, doCleanup) => {
16
42
  str = `${str}`
17
43
  }
18
44
  }
19
- if (str && doCleanup) {
20
- // remove html tags
21
- str = str.replace(/<p[^>]*>/g, ' ')
22
- str = str.replace(/<\/p>/g, ' ')
23
- str = str.replace(/<br[^>]*>/g, ' ')
24
- str = str.replace(/<[^>]*>/g, '')
25
- /* eslint-disable no-control-regex */
26
- // remove not printable characters
27
- str = str.replace(/[\x00-\x1F\x7F]/g, ' ')
28
- /* eslint-enable no-control-regex */
29
- // replace html entities
30
- str = str
31
- .replace(/&amp;/g, '&')
32
- .replace(/&lt;/g, '<')
33
- .replace(/&gt;/g, '>')
34
- .replace(/&#39;/g, '\'')
35
- .replace(/&quot;/g, '"')
36
- // replace two spaces with one
37
- str = str.replace(/\s+/g, ' ')
38
-
39
- str = str.split('\n').map(s => s.trim()).join('\n').trim()
45
+
46
+ if (str) {
47
+ if (basic) {
48
+ // remove html tags
49
+ str = str.replace(/<p[^>]*>/g, ' ')
50
+ str = str.replace(/<\/p>/g, ' ')
51
+ str = str.replace(/<br[^>]*>/g, ' ')
52
+ str = str.replace(/<[^>]*>/g, '')
53
+ /* eslint-disable no-control-regex */
54
+ // remove not printable characters
55
+ str = str.replace(/[\x00-\x1F\x7F]/g, ' ')
56
+ /* eslint-enable no-control-regex */
57
+ // replace html entities
58
+ str = str
59
+ .replace(/&amp;/g, '&')
60
+ .replace(/&lt;/g, '<')
61
+ .replace(/&gt;/g, '>')
62
+ .replace(/&#39;/g, '\'')
63
+ .replace(/&quot;/g, '"')
64
+ // replace two spaces with one
65
+ str = str.replace(/\s+/g, ' ')
66
+
67
+ str = str.split('\n').map(s => s.trim()).join('\n').trim()
68
+ }
69
+ if (charactersRemove) {
70
+ for (const character of charactersRemove) {
71
+ str = str.split(character).join('')
72
+ }
73
+ }
74
+ if (regexpRemove) {
75
+ str = str.replace(regexpRemove, '')
76
+ }
40
77
  }
41
78
  return str
42
79
  }
@@ -24,9 +24,9 @@ const _ = require('lodash')
24
24
  module.exports = class LogicHookUtils {
25
25
  constructor ({ buildScriptContext, caps }) {
26
26
  this.asserters = {}
27
- this.globalAsserters = []
27
+ this.globalAsserterNames = []
28
28
  this.logicHooks = {}
29
- this.globalLogicHooks = []
29
+ this.globalLogicHookNames = []
30
30
  this.userInputs = {}
31
31
  this.buildScriptContext = buildScriptContext
32
32
  this.caps = caps
@@ -64,7 +64,7 @@ module.exports = class LogicHookUtils {
64
64
  }
65
65
  this.asserters[asserter.ref] = this._loadClass(asserter, 'asserter')
66
66
  if (asserter.global) {
67
- this.globalAsserters.push(asserter.ref)
67
+ this.globalAsserterNames.push(asserter.ref)
68
68
  }
69
69
  })
70
70
  }
@@ -77,7 +77,7 @@ module.exports = class LogicHookUtils {
77
77
  }
78
78
  this.logicHooks[logicHook.ref] = this._loadClass(logicHook, 'logichook')
79
79
  if (logicHook.global) {
80
- this.globalLogicHooks.push(logicHook.ref)
80
+ this.globalLogicHookNames.push(logicHook.ref)
81
81
  }
82
82
  })
83
83
  }
@@ -92,14 +92,12 @@ module.exports = class LogicHookUtils {
92
92
  })
93
93
  }
94
94
 
95
- getGlobalAsserter () {
96
- return this.globalAsserters
97
- .map(name => this.asserters[name])
95
+ getGlobalAsserters () {
96
+ return this.globalAsserterNames.reduce((agg, name) => ({ ...agg, [name]: this.asserters[name] }), {})
98
97
  }
99
98
 
100
- getGlobalLogicHook () {
101
- return this.globalLogicHooks
102
- .map(name => this.logicHooks[name])
99
+ getGlobalLogicHooks () {
100
+ return this.globalLogicHookNames.reduce((agg, name) => ({ ...agg, [name]: this.logicHooks[name] }), {})
103
101
  }
104
102
 
105
103
  _loadClass ({ src, ref, args }, hookType) {
@@ -1,4 +1,3 @@
1
- const Capabilities = require('../../../Capabilities')
2
1
  const { BotiumError } = require('../../BotiumError')
3
2
  const { normalizeText, toString } = require('../../helper')
4
3
  const _ = require('lodash')
@@ -16,7 +15,7 @@ module.exports = class BaseTextAsserter {
16
15
  }
17
16
 
18
17
  _checkNormalizeText (str) {
19
- return normalizeText(str, !!this.caps[Capabilities.SCRIPTING_NORMALIZE_TEXT])
18
+ return normalizeText(str, this.caps)
20
19
  }
21
20
 
22
21
  _normalize (botresponse) {
@@ -1,5 +1,4 @@
1
1
  const _ = require('lodash')
2
- const { SCRIPTING_NORMALIZE_TEXT } = require('../../../Capabilities')
3
2
  const { BotiumError } = require('../../BotiumError')
4
3
  const { buttonsFromMsg } = require('../helpers')
5
4
  const { normalizeText } = require('../../helper')
@@ -12,7 +11,7 @@ module.exports = class ButtonsAsserter {
12
11
  }
13
12
 
14
13
  _evalButtons (args, botMsg) {
15
- const allButtons = buttonsFromMsg(botMsg, true).map(b => ({ text: b.text, payload: b.payload })).filter(b => b).map(b => ({ text: normalizeText(b.text, !!this.caps[SCRIPTING_NORMALIZE_TEXT]), payload: b.payload }))
14
+ const allButtons = buttonsFromMsg(botMsg, true).map(b => ({ text: b.text, payload: b.payload })).filter(b => b).map(b => ({ text: normalizeText(b.text, this.caps), payload: b.payload }))
16
15
  if (!args || args.length === 0) {
17
16
  return { allButtons, buttonsNotFound: [], buttonsFound: allButtons.map(b => b.text) }
18
17
  }
@@ -29,8 +28,8 @@ module.exports = class ButtonsAsserter {
29
28
  }
30
29
  }
31
30
  for (let i = 0; i < (args || []).length; i++) {
32
- const matchByText = allButtons.some(b => this.context.Match(b.text, normalizeText(args[i], !!this.caps[SCRIPTING_NORMALIZE_TEXT])))
33
- const matchByPayload = allButtons.some(b => this.context.Match(stringifyPayload(b.payload), normalizeText(args[i], !!this.caps[SCRIPTING_NORMALIZE_TEXT])))
31
+ const matchByText = allButtons.some(b => this.context.Match(b.text, normalizeText(args[i], this.caps)))
32
+ const matchByPayload = allButtons.some(b => this.context.Match(stringifyPayload(b.payload), normalizeText(args[i], this.caps)))
34
33
  if (matchByText || matchByPayload) {
35
34
  buttonsFound.push(args[i])
36
35
  } else {
@@ -1,7 +1,6 @@
1
1
  const fs = require('fs')
2
2
  const path = require('path')
3
- const globby = require('globby')
4
- const request = require('request')
3
+ const globSync = require('tinyglobby').globSync
5
4
  const mime = require('mime-types')
6
5
  const url = require('url')
7
6
  const _ = require('lodash')
@@ -119,24 +118,22 @@ module.exports = class MediaInput {
119
118
  throw new Error(`downloadMedia failed: ${err.message}`)
120
119
  }
121
120
  } else if (uri.protocol === 'http:' || uri.protocol === 'https:') {
122
- return new Promise((resolve, reject) => {
123
- request({
124
- uri: uri.toString(),
121
+ try {
122
+ const response = await fetch(uri.toString(), {
125
123
  method: 'GET',
126
- followAllRedirects: true,
127
- encoding: null,
128
- timeout: this.globalArgs.downloadTimeout || 10000
129
- }, (err, response, body) => {
130
- if (err) {
131
- reject(new Error(`downloadMedia failed: ${err.message}`))
132
- } else {
133
- if (response.statusCode >= 400) {
134
- return reject(new Error(`downloadMedia failed: ${response.statusCode}/${response.statusMessage}`))
135
- }
136
- resolve(body)
137
- }
124
+ redirect: 'follow', // Follows all redirects
125
+ timeout: this.globalArgs?.downloadTimeout || 10000
138
126
  })
139
- })
127
+
128
+ if (!response.ok) {
129
+ throw new Error(`downloadMedia failed: ${response.status}/${response.statusText}`)
130
+ }
131
+
132
+ const arrayBuffer = await response.arrayBuffer()
133
+ return Buffer.from(arrayBuffer)
134
+ } catch (err) {
135
+ throw new Error(`downloadMedia failed: ${err.message}`)
136
+ }
140
137
  } else if (uri.protocol === 'data:') {
141
138
  return Buffer.from(uri.href.split(',')[1], 'base64')
142
139
  }
@@ -161,7 +158,7 @@ module.exports = class MediaInput {
161
158
  const baseDir = this._getBaseDir(convo)
162
159
  return args.reduce((e, arg) => {
163
160
  if (this._isWildcard(arg)) {
164
- const mediaFiles = globby.sync(arg, { cwd: baseDir, gitignore: true })
161
+ const mediaFiles = globSync(arg, { cwd: baseDir })
165
162
  mediaFiles.forEach(mf => {
166
163
  e.push({
167
164
  name: 'MEDIA',
@@ -252,6 +252,24 @@ describe('compiler.compilertxt', function () {
252
252
  assert.equal(convo.conversation[0].logicHooks.length, 0)
253
253
  assert.equal(convo.conversation[1].messageText, 'Hi')
254
254
  })
255
+ // connectors might handle json format differently
256
+ it('should read json as message', async function () {
257
+ const scriptBuffer = fs.readFileSync(path.resolve(__dirname, CONVOS_DIR, 'convos_jsonmessage.convo.txt'))
258
+ const context = buildContext()
259
+
260
+ const caps = {}
261
+ const compiler = new Compiler(context, Object.assign({}, DefaultCapabilities, caps))
262
+
263
+ compiler.Compile(scriptBuffer, 'SCRIPTING_TYPE_CONVO')
264
+ assert.deepEqual(context.convos[0].conversation[0].sourceData, {
265
+ sessionId: '1234567890876543',
266
+ text: 'Text message',
267
+ data: {
268
+ key: 'value'
269
+ }
270
+ })
271
+ assert.equal(!!context.convos[0].conversation[0].messageText, false)
272
+ })
255
273
 
256
274
  describe('compiler.compilertxt.logichooks', function () {
257
275
  it('should read logicHook if there is just logicHook', async function () {
@@ -0,0 +1,10 @@
1
+ JSON
2
+
3
+ #me
4
+ {
5
+ "sessionId": "1234567890876543",
6
+ "text": "Text message",
7
+ "data": {
8
+ "key": "value"
9
+ }
10
+ }