<commit_before>import { BotpressEvent } from 'botpress-module-sdk'
import _ from 'lodash'
import 'reflect-metadata'

import { createSpyObject } from '../../misc/utils'

import { DialogEngine } from './dialog-engine'
import { context, flows, session } from './stubs'

const SESSION_ID = 'some_user_id'

describe('Dialog Engine', () => {
  const sessionService = createSpyObject('getSession', 'createSession', 'updateSession')
  const flowService = createSpyObject('loadAll')
  const instructionFactory = createSpyObject('createWait')
  const instructionProcessor = createSpyObject('process')

  const event: BotpressEvent = {
    type: 'any',
    target: 'any',
    direction: 'incoming',
    channel: 'any'
  }

  describe('Process instructions', () => {
    it('Call the instruction processor', async () => {
      givenInstructionsAreSuccessful()
      const dialogEngine = new DialogEngine(instructionFactory, instructionProcessor, flowService, sessionService)
      dialogEngine.currentSession = stubSession()
      dialogEngine.instructions = [{ type: 'on-enter', fn: () => {} }]

      await dialogEngine.processInstructions()

      expect(instructionProcessor.process).toHaveBeenCalled()
    })

    it('Stop processing on "wait" instruction', async () => {
      givenInstructionsAreSuccessful()
      const dialogEngine = new DialogEngine(instructionFactory, instructionProcessor, flowService, sessionService)
      dialogEngine.currentSession = stubSession()
      dialogEngine.instructions = [{ type: 'on-receive' }, { type: 'wait' }, { type: 'on-enter' }]

      await dialogEngine.processInstructions()

      expect(dialogEngine.instructions).toEqual([{ type: 'on-receive' }])
    })

    it('Wait on fail and retry the failed instruction', async () => {
      givenInstructionsAreSuccessful(false)
      givenWaitInstruction()
      const dialogEngine = new DialogEngine(instructionFactory, instructionProcessor, flowService, sessionService)
      dialogEngine.currentSession = stubSession()
      dialogEngine.instructions = [{ type: 'on-enter' }]

      await dialogEngine.processInstructions()

      expect(dialogEngine.instructions).toEqual([{ type: 'on-enter' }])
    })

    it('Update failed attempts', async () => {
      givenInstructionsAreSuccessful(false)
      givenWaitInstruction()
      const dialogEngine = new DialogEngine(instructionFactory, instructionProcessor, flowService, sessionService)
      dialogEngine.currentSession = stubSession()
      dialogEngine.instructions = [{ type: 'on-enter' }, { type: 'on-enter' }, { type: 'on-enter' }]

      await dialogEngine.processInstructions()

      expect(dialogEngine.failedAttempts).toEqual(3)
    })

    it('Reset failed attempts on successful process', async () => {
      givenInstructionsAreSuccessful()
      givenWaitInstruction()
      const dialogEngine = new DialogEngine(instructionFactory, instructionProcessor, flowService, sessionService)
      dialogEngine.currentSession = stubSession()
      dialogEngine.instructions = [{ type: 'on-enter' }]
      dialogEngine.failedAttempts = 5

      await dialogEngine.processInstructions()

      expect(dialogEngine.failedAttempts).toEqual(0)
    })

    it('Throw on max failed attempts', async () => {
      givenInstructionsAreSuccessful(false)
      givenWaitInstruction()
      const dialogEngine = new DialogEngine(instructionFactory, instructionProcessor, flowService, sessionService)
      dialogEngine.currentSession = stubSession()
      dialogEngine.instructions = [{ type: 'on-enter' }]
      dialogEngine.failedAttempts = 9

      // Work around issue of expecting throw on async functions
      // see: https://github.com/facebook/jest/issues/1700
      expect(dialogEngine.processInstructions()).rejects.toEqual(new Error('Too many instructions failed'))
    })

    it('Transit to next node if condition is sucessful', async () => {
      givenInstructionsAreSuccessful()
      const dialogEngine = new DialogEngine(instructionFactory, instructionProcessor, flowService, sessionService)
      dialogEngine.currentSession = stubSession()
      dialogEngine.instructions = [{ type: 'transition-condition', node: 'another-node' }]
      const spy = spyOn(dialogEngine, 'transitionToNextNode')

      await dialogEngine.processInstructions()

      expect(spy).toHaveBeenCalledWith('another-node')
    })
  })

  describe('Transit to another node', () => {
    let dialogEngine: DialogEngine

    beforeEach(() => {
      dialogEngine = new DialogEngine(instructionFactory, instructionProcessor, flowService, sessionService)
      dialogEngine.currentSession = stubSession()
      dialogEngine.flows = flows
    })

    it('Assign current node as the next node in the current flow', async () => {
      await dialogEngine.transitionToNextNode('welcome')

      expect(dialogEngine.currentSession.context.currentNode.name).toEqual('welcome')
      expect(dialogEngine.currentSession.context.currentFlow.name).toEqual('main.flow.json')
      expect(sessionService.updateSession).toHaveBeenCalled()
    })

    it('Assign current node as the starting node of the next flow', async () => {
      await dialogEngine.transitionToNextNode('other.flow.json')

      expect(dialogEngine.currentSession.context.currentNode.name).toEqual('entry')
      expect(dialogEngine.currentSession.context.currentFlow.name).toEqual('other.flow.json')
      expect(sessionService.updateSession).toHaveBeenCalled()
    })

    it('Assign current node as the last called node in the parent flow', async () => {
      await dialogEngine.transitionToNextNode('other.flow.json')
      await dialogEngine.transitionToNextNode('##')

      expect(dialogEngine.currentSession.context.currentNode.name).toEqual('entry')
      expect(dialogEngine.currentSession.context.currentFlow.name).toEqual('main.flow.json')
    })

    it('Assign current node as a specific node of the parent flow', async () => {
      await dialogEngine.transitionToNextNode('other.flow.json')
      await dialogEngine.transitionToNextNode('#welcome')

      expect(dialogEngine.currentSession.context.currentNode.name).toEqual('welcome')
      expect(dialogEngine.currentSession.context.currentFlow.name).toEqual('main.flow.json')
    })
  })

  function givenWaitInstruction() {
    instructionFactory.createWait.mockReturnValue({ type: 'wait' })
  }

  function givenInstructionsAreSuccessful(success: boolean = true) {
    instructionProcessor.process.mockReturnValue(success)
  }

  function stubSession() {
    return { id: 'an_id', context: context, event: '' }
  }
})
<commit_msg>Test when node or flow is not found<commit_after>import { BotpressEvent } from 'botpress-module-sdk'
import _ from 'lodash'
import 'reflect-metadata'

import { createSpyObject } from '../../misc/utils'

import { DialogEngine } from './dialog-engine'
import { context, flows, session } from './stubs'

const SESSION_ID = 'some_user_id'

describe('Dialog Engine', () => {
  const sessionService = createSpyObject('getSession', 'createSession', 'updateSession')
  const flowService = createSpyObject('loadAll')
  const instructionFactory = createSpyObject('createWait')
  const instructionProcessor = createSpyObject('process')

  const event: BotpressEvent = {
    type: 'any',
    target: 'any',
    direction: 'incoming',
    channel: 'any'
  }

  describe('Process instructions', () => {
    it('Call the instruction processor', async () => {
      givenInstructionsAreSuccessful()
      const dialogEngine = new DialogEngine(instructionFactory, instructionProcessor, flowService, sessionService)
      dialogEngine.currentSession = stubSession()
      dialogEngine.instructions = [{ type: 'on-enter', fn: () => {} }]

      await dialogEngine.processInstructions()

      expect(instructionProcessor.process).toHaveBeenCalled()
    })

    it('Stop processing on "wait" instruction', async () => {
      givenInstructionsAreSuccessful()
      const dialogEngine = new DialogEngine(instructionFactory, instructionProcessor, flowService, sessionService)
      dialogEngine.currentSession = stubSession()
      dialogEngine.instructions = [{ type: 'on-receive' }, { type: 'wait' }, { type: 'on-enter' }]

      await dialogEngine.processInstructions()

      expect(dialogEngine.instructions).toEqual([{ type: 'on-receive' }])
    })

    it('Wait on fail and retry the failed instruction', async () => {
      givenInstructionsAreSuccessful(false)
      givenWaitInstruction()
      const dialogEngine = new DialogEngine(instructionFactory, instructionProcessor, flowService, sessionService)
      dialogEngine.currentSession = stubSession()
      dialogEngine.instructions = [{ type: 'on-enter' }]

      await dialogEngine.processInstructions()

      expect(dialogEngine.instructions).toEqual([{ type: 'on-enter' }])
    })

    it('Update failed attempts', async () => {
      givenInstructionsAreSuccessful(false)
      givenWaitInstruction()
      const dialogEngine = new DialogEngine(instructionFactory, instructionProcessor, flowService, sessionService)
      dialogEngine.currentSession = stubSession()
      dialogEngine.instructions = [{ type: 'on-enter' }, { type: 'on-enter' }, { type: 'on-enter' }]

      await dialogEngine.processInstructions()

      expect(dialogEngine.failedAttempts).toEqual(3)
    })

    it('Reset failed attempts on successful process', async () => {
      givenInstructionsAreSuccessful()
      givenWaitInstruction()
      const dialogEngine = new DialogEngine(instructionFactory, instructionProcessor, flowService, sessionService)
      dialogEngine.currentSession = stubSession()
      dialogEngine.instructions = [{ type: 'on-enter' }]
      dialogEngine.failedAttempts = 5

      await dialogEngine.processInstructions()

      expect(dialogEngine.failedAttempts).toEqual(0)
    })

    it('Throw on max failed attempts', async () => {
      givenInstructionsAreSuccessful(false)
      givenWaitInstruction()
      const dialogEngine = new DialogEngine(instructionFactory, instructionProcessor, flowService, sessionService)
      dialogEngine.currentSession = stubSession()
      dialogEngine.instructions = [{ type: 'on-enter' }]
      dialogEngine.failedAttempts = 9

      // Work around issue of expecting throw on async functions
      // see: https://github.com/facebook/jest/issues/1700
      expect(dialogEngine.processInstructions()).rejects.toEqual(new Error('Too many instructions failed'))
    })

    it('Transit to next node if condition is sucessful', async () => {
      givenInstructionsAreSuccessful()
      const dialogEngine = new DialogEngine(instructionFactory, instructionProcessor, flowService, sessionService)
      dialogEngine.currentSession = stubSession()
      dialogEngine.instructions = [{ type: 'transition-condition', node: 'another-node' }]
      const spy = spyOn(dialogEngine, 'transitionToNextNode')

      await dialogEngine.processInstructions()

      expect(spy).toHaveBeenCalledWith('another-node')
    })
  })

  describe('Transit to another node', () => {
    let dialogEngine: DialogEngine

    beforeEach(() => {
      dialogEngine = new DialogEngine(instructionFactory, instructionProcessor, flowService, sessionService)
      dialogEngine.currentSession = stubSession()
      dialogEngine.flows = flows
    })

    it('Assign current node as the next node in the current flow', async () => {
      await dialogEngine.transitionToNextNode('welcome')

      expect(dialogEngine.currentSession.context.currentNode.name).toEqual('welcome')
      expect(dialogEngine.currentSession.context.currentFlow.name).toEqual('main.flow.json')
      expect(sessionService.updateSession).toHaveBeenCalled()
    })

    it('Assign current node as the starting node of the next flow', async () => {
      await dialogEngine.transitionToNextNode('other.flow.json')

      expect(dialogEngine.currentSession.context.currentNode.name).toEqual('entry')
      expect(dialogEngine.currentSession.context.currentFlow.name).toEqual('other.flow.json')
      expect(sessionService.updateSession).toHaveBeenCalled()
    })

    it('Assign current node as the last called node in the parent flow', async () => {
      await dialogEngine.transitionToNextNode('other.flow.json')
      await dialogEngine.transitionToNextNode('##')

      expect(dialogEngine.currentSession.context.currentNode.name).toEqual('entry')
      expect(dialogEngine.currentSession.context.currentFlow.name).toEqual('main.flow.json')
    })

    it('Assign current node as a specific node of the parent flow', async () => {
      await dialogEngine.transitionToNextNode('other.flow.json')
      await dialogEngine.transitionToNextNode('#welcome')

      expect(dialogEngine.currentSession.context.currentNode.name).toEqual('welcome')
      expect(dialogEngine.currentSession.context.currentFlow.name).toEqual('main.flow.json')
    })

    it('Throws when the node or the flow doesnt exists', () => {
      expect(dialogEngine.transitionToNextNode('unknown')).rejects.toEqual(
        new Error('Could not find any node or flow under the name of "unknown"')
      )
    })
  })

  function givenWaitInstruction() {
    instructionFactory.createWait.mockReturnValue({ type: 'wait' })
  }

  function givenInstructionsAreSuccessful(success: boolean = true) {
    instructionProcessor.process.mockReturnValue(success)
  }

  function stubSession() {
    return { id: 'an_id', context: context, event: '' }
  }
})

{'fraction_non_alphanumeric': 0.11521286097052694, 'fraction_numerical': 0.0011908306043465317, 'mean_word_length': 4.188030888030888, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 36, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7656358', 'n_tokens_mistral': 3539, 'n_tokens_neox': 3490, 'n_words': 750}