UNPKG

@yoctol/kurator

Version:

1,788 lines (1,556 loc) 65.4 kB
/* eslint-disable camelcase */ const nock = require('nock'); const B = require('bottender-compose'); const warning = require('warning'); const { MessengerClient } = require('messaging-api-messenger'); const { Client: Understood } = require('ynlu'); const Kurator = require('../Kurator'); const definitionBasic = require('../__fixtures__/definitions/basic'); const definitionProfileWithExtensionsEnabled = require('../__fixtures__/definitions/profileWithExtensionsEnabled'); const definition_0_6_4 = require('../__fixtures__/definition-0.6.4'); const definitionSwitchToHumanActions = require('../__fixtures__/definitions/switch-to-human-actions'); const definitionSystemActions = require('../__fixtures__/definitions/system-actions'); const definitionLineSystemActions = require('../__fixtures__/definitions/line/system-actions'); const definitionUnderstoodTriggerIntent = require('../__fixtures__/definitions/understood-trigger-INTENT'); const definitionUnderstoodTriggerEntity = require('../__fixtures__/definitions/understood-trigger-ENTITY'); const definitionUnderstoodTriggerEntityWithEntityValue = require('../__fixtures__/definitions/understood-trigger-ENTITY-with-entity-value'); const definitionUnderstoodTriggerIntentEntity = require('../__fixtures__/definitions/understood-trigger-INTENT_ENTITY'); const definitionUnderstoodTriggerIntentEntityWithEntityValue = require('../__fixtures__/definitions/understood-trigger-INTENT_ENTITY-with-entity-value'); const YOCTOL_AI_APP_ID = '1869280723119673'; jest.mock('messaging-api-messenger'); jest.mock('warning'); jest.mock('ynlu'); afterEach(() => { nock.cleanAll(); }); async function setup({ definition, hasUpdate, messengerProfile = {}, ...options } = {}) { nock('https://kurator.yoctol.com') .persist() .get('/api/projects/1/definition') .reply(200, definition || definitionBasic); nock('https://kurator.yoctol.com') .persist() .get( '/api/projects/1/check-update?hash=86c410ddb220074ed7eda61c1e3bf9696b4ee798' ) .reply(200, { hasUpdate: !!hasUpdate }); const messenger = { getMessengerProfile: jest.fn(), setMessengerProfile: jest.fn(), deleteMessengerProfile: jest.fn(), }; MessengerClient.connect.mockReturnValue(messenger); messenger.getMessengerProfile.mockResolvedValue([messengerProfile]); const classifier = { predict: jest.fn(), }; const understood = { findClassifierById: jest.fn(), }; understood.findClassifierById.mockReturnValue(classifier); Understood.connect.mockReturnValue(understood); const kurator = new Kurator({ projectId: '1', accessToken: 'ACCESS_TOKEN', ...options, }); // use this internal promise to make sure request is finished await kurator._initPromise; const middleware = kurator.createBottenderMiddleware(); const next = jest.fn(); const handler = kurator.createBottenderHandler(); return { kurator, understood, classifier, middleware, next, handler, messenger, }; } it('should sync definition when construct', async () => { const { kurator } = await setup(); expect(kurator.getAction('1')).toBeDefined(); }); it('should use local definition to construct kurator when remote definition fail', async () => { nock('https://kurator.yoctol.com') .persist() .get('/api/projects/1/definition') .reply(500); const kurator = new Kurator({ projectId: '1', accessToken: 'ACCESS_TOKEN', definition: definition_0_6_4, }); const context = { platform: 'messenger', event: { isText: true, isPayload: false, text: '1a2B3C', }, sendText: jest.fn(), }; const action = kurator.getAction('280609056182244758'); await action(context); expect(context.sendText).toBeCalledWith('不應該啊', {}); }); describe('#getAction', () => { it('should have displayName', async () => { const { kurator } = await setup(); expect(kurator.getAction('1')).toHaveProperty('displayName', '測試'); }); it('should be executable bottender action', async () => { const { kurator } = await setup(); const context = { platform: 'messenger', sendText: jest.fn(), }; const action = kurator.getAction('1'); await action(context); expect(context.sendText).toBeCalledWith('基本的', {}); }); it('should support actionMap', async () => { const { kurator } = await setup({ actionMap: { 測試: '1' } }); const context = { platform: 'messenger', sendText: jest.fn(), }; const action = kurator.getAction('測試'); await action(context); expect(context.sendText).toBeCalledWith('基本的', {}); }); }); describe('#getRemoteActionIds', () => { it('should retrun an array of ids', async () => { const { kurator } = await setup(); expect(kurator.getRemoteActionIds()).toEqual(['1', '269736288330978969']); }); }); describe('#registerAction', () => { it('should support local action', async () => { const { kurator } = await setup(); const context = { platform: 'messenger', sendText: jest.fn(), }; kurator.registerAction('不要騙我', B.sendText('不要騙我')); const action = kurator.getAction('不要騙我'); await action(context); expect(context.sendText).toBeCalledWith('不要騙我'); }); it('should have displayName', async () => { const { kurator } = await setup(); kurator.registerAction('不要騙我', B.sendText('不要騙我')); expect(kurator.getAction('不要騙我')).toHaveProperty( 'displayName', '不要騙我' ); }); }); describe('#registerActions', () => { it('should support local action', async () => { const { kurator } = await setup(); const context = { platform: 'messenger', sendText: jest.fn(), }; kurator.registerActions({ 不要騙我: B.sendText('不要騙我'), 不要騙我2: B.sendText('不要騙我2'), }); const action = kurator.getAction('不要騙我'); await action(context); expect(context.sendText).toBeCalledWith('不要騙我'); }); }); describe('#createBottenderHandler', () => { it('should support id link', async () => { const { handler } = await setup(); const context = { platform: 'messenger', event: { isText: false, isPayload: true, payload: '1', }, sendText: jest.fn(), }; await handler(context); expect(context.sendText).toBeCalledWith('基本的', {}); }); }); describe('#createBottenderMiddleware', () => { describe('isFollow', () => { it('should support get started', async () => { const { middleware, next } = await setup({ definition: definitionLineSystemActions, }); const context = { platform: 'line', event: { isFollow: true, }, sendText: jest.fn(), }; await middleware(context, next); expect(context.sendText).toBeCalledWith( '您好,我是機器人小助手,請問有什麼我能幫忙的呢?', {} ); expect(next).not.toBeCalled(); }); }); describe('payload', () => { it('should support get started', async () => { const { middleware, next } = await setup({ definition: definitionSystemActions, }); const context = { platform: 'messenger', event: { isText: false, isPayload: true, payload: '__GET_STARTED__', }, sendText: jest.fn(), }; await middleware(context, next); expect(context.sendText).toBeCalledWith( '您好,我是機器人小助手,請問有什麼我能幫忙的呢?', {} ); expect(next).not.toBeCalled(); }); it('should support id link', async () => { const { middleware, next } = await setup(); const context = { platform: 'messenger', event: { isText: false, isPayload: true, payload: '1', }, sendText: jest.fn(), }; await middleware(context, next); expect(context.sendText).toBeCalledWith('基本的', {}); expect(next).not.toBeCalled(); }); it('should call next when no match id', async () => { const { middleware, next } = await setup(); const context = { platform: 'messenger', event: { isText: false, isPayload: true, payload: '10000000000', }, sendText: jest.fn(), }; await middleware(context, next); expect(context.sendText).not.toBeCalled(); expect(next).toBeCalled(); }); }); describe('fallback action', () => { it('should support fallback action', async () => { const { middleware, next } = await setup({ definition: definitionSystemActions, }); const context = { platform: 'messenger', event: { isText: true, isPayload: false, text: '不可能中', }, sendText: jest.fn(), }; await middleware(context, next); expect(context.sendText).toBeCalledWith('抱歉我聽不懂', {}); expect(next).not.toBeCalled(); }); it('should support skipFallbackAction', async () => { const { middleware, next } = await setup({ definition: definitionSystemActions, skipFallbackAction: true, }); const context = { platform: 'messenger', event: { isText: true, isPayload: false, text: '不可能中', }, sendText: jest.fn(), }; await middleware(context, next); expect(context.sendText).not.toBeCalled(); expect(next).toBeCalled(); }); }); describe('#trigger', () => { describe('#keyword', () => { it('should support keyword (version >0.5.52)', async () => { const { middleware, next } = await setup({ definition: definition_0_6_4, }); const context = { platform: 'messenger', event: { isText: true, isPayload: false, text: '1a2B3C', }, sendText: jest.fn(), }; await middleware(context, next); expect(context.sendText).toBeCalledWith('不應該啊', {}); expect(next).not.toBeCalled(); }); it('should trigger action when all keywords matched when rule ALL setted', async () => { const { middleware, next } = await setup({ definition: definition_0_6_4, }); const context = { platform: 'messenger', event: { isText: true, isPayload: false, text: 'xyz', }, sendText: jest.fn(), }; await middleware(context, next); expect(context.sendText).toBeCalledWith('不應該啊', {}); expect(next).not.toBeCalled(); }); it('should not trigger when not all keywords matched when rule ALL setted', async () => { const { middleware, next } = await setup({ definition: definition_0_6_4, }); const context = { platform: 'messenger', event: { isText: true, isPayload: false, text: 'xYz', }, sendText: jest.fn(), }; await middleware(context, next); expect(context.sendText).not.toBeCalled(); expect(next).toBeCalled(); }); }); describe('#referral', () => { it('should support referral (version >0.5.52)', async () => { const { middleware, next } = await setup({ definition: definition_0_6_4, }); const context = { platform: 'messenger', event: { isReferral: true, referral: { ref: 'testRef', }, }, sendText: jest.fn(), }; await middleware(context, next); expect(context.sendText).toBeCalledWith('不應該啊', {}); expect(next).not.toBeCalled(); }); }); describe('#regexp', () => { it('should support triggers regexp', async () => { const { middleware, next } = await setup({ definition: definition_0_6_4, }); const context = { platform: 'messenger', event: { isText: true, isPayload: false, text: '1234567890', }, sendText: jest.fn(), }; await middleware(context, next); expect(context.sendText).toBeCalledWith('不應該啊', {}); expect(next).not.toBeCalled(); }); it('should call next when no match regexp', async () => { const { middleware, next } = await setup(); const context = { platform: 'messenger', event: { isText: true, isPayload: false, text: '?????', }, sendText: jest.fn(), }; await middleware(context, next); expect(context.sendText).not.toBeCalled(); expect(next).toBeCalled(); }); }); describe('#understood', () => { describe('#INTENT', () => { it('should match top intent whose score is over 45%', async () => { const { middleware, next, understood, classifier } = await setup({ definition: definitionUnderstoodTriggerIntent, }); classifier.predict.mockResolvedValue({ intents: [ { id: 123456789, name: '黑人問號', score: 0.8, }, { id: 234567891, name: '白人問號', score: 0.15, }, { id: 345678912, name: '小人問號', score: 0.05, }, ], entities: [], match: { isMatched: true, score: 0.1, }, }); const context = { platform: 'messenger', event: { isText: true, isPayload: false, text: '?????', }, sendText: jest.fn(), }; await middleware(context, next); expect(understood.findClassifierById).toBeCalledWith( '305814150547374820' ); expect(classifier.predict).toBeCalledWith('?????'); expect(context.sendText).toBeCalledWith('不應該啊', {}); expect(next).not.toBeCalled(); }); it('should match top intent whose match.isMatched = true', async () => { const { middleware, next, understood, classifier } = await setup({ definition: definitionUnderstoodTriggerIntent, useDeprecatedIntentThreshold: false, }); classifier.predict.mockResolvedValue({ intents: [ { id: 123456789, name: '黑人問號', score: 0.4, }, { id: 234567891, name: '白人問號', score: 0.15, }, { id: 345678912, name: '小人問號', score: 0.05, }, ], entities: [], match: { isMatched: true, score: 0.1, }, }); const context = { platform: 'messenger', event: { isText: true, isPayload: false, text: '?????', }, sendText: jest.fn(), }; await middleware(context, next); expect(understood.findClassifierById).toBeCalledWith( '305814150547374820' ); expect(classifier.predict).toBeCalledWith('?????'); expect(context.sendText).toBeCalledWith('不應該啊', {}); expect(next).not.toBeCalled(); }); it('should call next when no match understood', async () => { const { middleware, next, classifier } = await setup({ definition: definitionUnderstoodTriggerIntent, }); classifier.predict.mockResolvedValue({ intents: [ { id: 123456789, name: '黑人問號', score: 0.4, }, { id: 234567891, name: '白人問號', score: 0.4, }, { id: 345678912, name: '小人問號', score: 0.2, }, ], entities: [], match: { isMatched: true, score: 0.1, }, }); const context = { platform: 'messenger', event: { isText: true, isPayload: false, text: '?????', }, sendText: jest.fn(), }; await middleware(context, next); expect(context.sendText).not.toBeCalled(); expect(next).toBeCalled(); }); }); describe('#ENTITY', () => { it('should match when entity exists', async () => { const { middleware, next, understood, classifier } = await setup({ definition: definitionUnderstoodTriggerEntity, }); classifier.predict.mockResolvedValue({ intents: [ { id: 123456789, name: '黑人問號', score: 0.8, }, { id: 234567891, name: '白人問號', score: 0.15, }, { id: 345678912, name: '小人問號', score: 0.05, }, ], entities: [ { id: 456789123, name: '標點符號', value: '問號', parsedResult: { type: 'entity_value', entityValueId: 876543210, value: '黑人問號', }, }, ], match: { isMatched: true, score: 0.1, }, }); const context = { platform: 'messenger', event: { isText: true, isPayload: false, text: '?????', }, sendText: jest.fn(), }; await middleware(context, next); expect(understood.findClassifierById).toBeCalledWith( '305814150547374820' ); expect(classifier.predict).toBeCalledWith('?????'); expect(context.sendText).toBeCalledWith('不應該啊', {}); expect(next).not.toBeCalled(); }); it('should not match when entity does not exist', async () => { const { middleware, next, understood, classifier } = await setup({ definition: definitionUnderstoodTriggerEntity, }); classifier.predict.mockResolvedValue({ intents: [ { id: 123456789, name: '黑人問號', score: 0.8, }, { id: 234567891, name: '白人問號', score: 0.15, }, { id: 345678912, name: '小人問號', score: 0.05, }, ], entities: [ { id: 556789123, name: '手機', value: 'iPhone X', parsedResult: { type: 'entity_value', entityValueId: 876543210, value: 'iPhone X', }, }, ], match: { isMatched: true, score: 0.1, }, }); const context = { platform: 'messenger', event: { isText: true, isPayload: false, text: '?????', }, sendText: jest.fn(), }; await middleware(context, next); expect(understood.findClassifierById).toBeCalledWith( '305814150547374820' ); expect(classifier.predict).toBeCalledWith('?????'); expect(context.sendText).not.toBeCalled(); expect(next).toBeCalled(); }); it('should match when entity value match', async () => { const { middleware, next, understood, classifier } = await setup({ definition: definitionUnderstoodTriggerIntentEntityWithEntityValue, }); classifier.predict.mockResolvedValue({ intents: [ { id: 123456789, name: '黑人問號', score: 0.8, }, { id: 234567891, name: '白人問號', score: 0.15, }, { id: 345678912, name: '小人問號', score: 0.05, }, ], entities: [ { id: 456789123, name: '飲料', value: '珍奶', parsedResult: { type: 'entity_value', entityValueId: 876543210, value: '珍珠奶茶', }, }, ], match: { isMatched: true, score: 0.1, }, }); const context = { platform: 'messenger', event: { isText: true, isPayload: false, text: '?????', }, sendText: jest.fn(), }; await middleware(context, next); expect(understood.findClassifierById).toBeCalledWith( '305814150547374820' ); expect(classifier.predict).toBeCalledWith('?????'); expect(context.sendText).toBeCalledWith('不應該啊', {}); expect(next).not.toBeCalled(); }); it('should not match when entity value does not exist', async () => { const { middleware, next, understood, classifier } = await setup({ definition: definitionUnderstoodTriggerEntityWithEntityValue, }); classifier.predict.mockResolvedValue({ intents: [ { id: 123456789, name: '黑人問號', score: 0.8, }, { id: 234567891, name: '白人問號', score: 0.15, }, { id: 345678912, name: '小人問號', score: 0.05, }, ], entities: [ { id: 456789123, name: '飲料', value: '珍奶', parsedResult: { type: 'entity_value', entityValueId: 11111111, value: '珍珠奶茶', }, }, ], match: { isMatched: true, score: 0.1, }, }); const context = { platform: 'messenger', event: { isText: true, isPayload: false, text: '?????', }, sendText: jest.fn(), }; await middleware(context, next); expect(understood.findClassifierById).toBeCalledWith( '305814150547374820' ); expect(classifier.predict).toBeCalledWith('?????'); expect(context.sendText).not.toBeCalled(); expect(next).toBeCalled(); }); it('should not match when no parsedResult', async () => { const { middleware, next, understood, classifier } = await setup({ definition: definitionUnderstoodTriggerEntityWithEntityValue, }); classifier.predict.mockResolvedValue({ intents: [ { id: 123456789, name: '黑人問號', score: 0.8, }, { id: 234567891, name: '白人問號', score: 0.15, }, { id: 345678912, name: '小人問號', score: 0.05, }, ], entities: [ { id: 456789123, name: '飲料', value: '珍奶', }, ], match: { isMatched: true, score: 0.1, }, }); const context = { platform: 'messenger', event: { isText: true, isPayload: false, text: '?????', }, sendText: jest.fn(), }; await middleware(context, next); expect(understood.findClassifierById).toBeCalledWith( '305814150547374820' ); expect(classifier.predict).toBeCalledWith('?????'); expect(context.sendText).not.toBeCalled(); expect(next).toBeCalled(); }); it('should work with props template', async () => { const { middleware, next, understood, classifier } = await setup({ definition: { version: '0.6.4', actions: { '280609056182244758': { id: '280609056182244758', projectId: '280608912233731476', name: '隨便的 Composite action name', createdAt: '2018-09-21T04:00:06.215Z', updatedAt: '2018-10-24T09:52:56.321Z', deletedAt: null, actionGroupId: '280608999567529365', broadcastId: null, triggers: [ { id: '304704150547374820', type: 'UNDERSTOOD', compositeActionId: '280609056182244758', createdAt: '2018-10-24T09:52:45.274Z', updatedAt: '2018-10-24T09:52:56.314Z', deletedAt: null, understood: { id: '304704176312020767', type: 'ENTITY', classifierId: '305814150547374820', entityId: 456789123, entityValueId: 876543210, createdAt: '2018-10-24T09:52:48.377Z', updatedAt: '2018-10-24T09:52:56.299Z', deletedAt: null, triggerId: '304704150547374820', }, }, ], actions: [ { id: '288543007458727346', order: 1, type: 'text', descriptor: { text: '{{props.飲料}}不應該啊', buttons: [], }, actionableId: '280609056182244758', actionableType: 'composite_actions', platform: 'messenger', createdAt: '2018-10-02T02:43:26.919Z', updatedAt: '2018-10-02T02:43:26.919Z', deletedAt: null, actions: [], }, ], }, }, persistentMenu: { id: 'QG1Q4b3G_', items: [], textfield: true, }, greetingText: '111', }, }); classifier.predict.mockResolvedValue({ intents: [ { id: 123456789, name: '黑人問號', score: 0.8, }, { id: 234567891, name: '白人問號', score: 0.15, }, { id: 345678912, name: '小人問號', score: 0.05, }, ], entities: [ { id: 456789123, name: '飲料', value: '珍奶', parsedResult: { type: 'entity_value', entityValueId: 876543210, value: '珍珠奶茶', }, }, ], match: { isMatched: true, score: 0.1, }, }); const context = { platform: 'messenger', event: { isText: true, isPayload: false, text: '?????', }, sendText: jest.fn(), }; await middleware(context, next); expect(understood.findClassifierById).toBeCalledWith( '305814150547374820' ); expect(classifier.predict).toBeCalledWith('?????'); expect(context.sendText).toBeCalledWith('珍珠奶茶不應該啊', {}); expect(next).not.toBeCalled(); }); }); describe('#INTENT_ENTITY', () => { it('should match both intent and entity', async () => { const { middleware, next, understood, classifier } = await setup({ definition: definitionUnderstoodTriggerIntentEntity, }); classifier.predict.mockResolvedValue({ intents: [ { id: 123456789, name: '黑人問號', score: 0.8, }, { id: 234567891, name: '白人問號', score: 0.15, }, { id: 345678912, name: '小人問號', score: 0.05, }, ], entities: [ { id: 456789123, name: '標點符號', value: '問號', parsedResult: { type: 'entity_value', entityValueId: 876543210, value: '黑人問號', }, }, ], match: { isMatched: true, score: 0.1, }, }); const context = { platform: 'messenger', event: { isText: true, isPayload: false, text: '?????', }, sendText: jest.fn(), }; await middleware(context, next); expect(understood.findClassifierById).toBeCalledWith( '305814150547374820' ); expect(classifier.predict).toBeCalledWith('?????'); expect(context.sendText).toBeCalledWith('不應該啊', {}); expect(next).not.toBeCalled(); }); it('should match when entity value match', async () => { const { middleware, next, understood, classifier } = await setup({ definition: definitionUnderstoodTriggerEntityWithEntityValue, }); classifier.predict.mockResolvedValue({ intents: [ { id: 123456789, name: '黑人問號', score: 0.8, }, { id: 234567891, name: '白人問號', score: 0.15, }, { id: 345678912, name: '小人問號', score: 0.05, }, ], entities: [ { id: 456789123, name: '飲料', value: '珍奶', parsedResult: { type: 'entity_value', entityValueId: 876543210, value: '珍珠奶茶', }, }, ], match: { isMatched: true, score: 0.1, }, }); const context = { platform: 'messenger', event: { isText: true, isPayload: false, text: '?????', }, sendText: jest.fn(), }; await middleware(context, next); expect(understood.findClassifierById).toBeCalledWith( '305814150547374820' ); expect(classifier.predict).toBeCalledWith('?????'); expect(context.sendText).toBeCalledWith('不應該啊', {}); expect(next).not.toBeCalled(); }); it('should not match when no parsedResult', async () => { const { middleware, next, classifier } = await setup({ definition: definitionUnderstoodTriggerIntentEntity, }); classifier.predict.mockResolvedValue({ intents: [ { id: 123456789, name: '黑人問號', score: 0.8, }, { id: 234567891, name: '白人問號', score: 0.15, }, { id: 345678912, name: '小人問號', score: 0.05, }, ], entities: [ { id: 456789123, name: '標點符號', value: '問號', }, ], match: { isMatched: true, score: 0.1, }, }); const context = { platform: 'messenger', event: { isText: true, isPayload: false, text: '?????', }, sendText: jest.fn(), }; await middleware(context, next); expect(context.sendText).not.toBeCalledWith('不應該啊', {}); expect(next).toBeCalled(); }); it('should work with props template', async () => { const { middleware, next, understood, classifier } = await setup({ definition: { version: '0.6.4', actions: { '280609056182244758': { id: '280609056182244758', projectId: '280608912233731476', name: '隨便的 Composite action name', createdAt: '2018-09-21T04:00:06.215Z', updatedAt: '2018-10-24T09:52:56.321Z', deletedAt: null, actionGroupId: '280608999567529365', broadcastId: null, triggers: [ { id: '304704150547374820', type: 'UNDERSTOOD', compositeActionId: '280609056182244758', createdAt: '2018-10-24T09:52:45.274Z', updatedAt: '2018-10-24T09:52:56.314Z', deletedAt: null, understood: { id: '304704176312020767', type: 'INTENT_ENTITY', classifierId: '305814150547374820', intentId: 123456789, entityId: 456789123, entityValueId: 876543210, createdAt: '2018-10-24T09:52:48.377Z', updatedAt: '2018-10-24T09:52:56.299Z', deletedAt: null, triggerId: '304704150547374820', }, }, ], actions: [ { id: '288543007458727346', order: 1, type: 'text', descriptor: { text: '{{props.飲料}}不應該啊', buttons: [], }, actionableId: '280609056182244758', actionableType: 'composite_actions', platform: 'messenger', createdAt: '2018-10-02T02:43:26.919Z', updatedAt: '2018-10-02T02:43:26.919Z', deletedAt: null, actions: [], }, ], }, }, persistentMenu: { id: 'QG1Q4b3G_', items: [], textfield: true, }, greetingText: '111', }, }); classifier.predict.mockResolvedValue({ intents: [ { id: 123456789, name: '黑人問號', score: 0.8, }, { id: 234567891, name: '白人問號', score: 0.15, }, { id: 345678912, name: '小人問號', score: 0.05, }, ], entities: [ { id: 456789123, name: '飲料', value: '珍奶', parsedResult: { type: 'entity_value', entityValueId: 876543210, value: '珍珠奶茶', }, }, ], match: { isMatched: true, score: 0.1, }, }); const context = { platform: 'messenger', event: { isText: true, isPayload: false, text: '?????', }, sendText: jest.fn(), }; await middleware(context, next); expect(understood.findClassifierById).toBeCalledWith( '305814150547374820' ); expect(classifier.predict).toBeCalledWith('?????'); expect(context.sendText).toBeCalledWith('珍珠奶茶不應該啊', {}); expect(next).not.toBeCalled(); }); }); }); }); it('should not handle other events', async () => { const { middleware, next } = await setup(); const context = { platform: 'messenger', event: { isText: false, isPayload: false, }, sendText: jest.fn(), }; await middleware(context, next); expect(context.sendText).not.toBeCalled(); expect(next).toBeCalled(); }); describe('switch to human actions', () => { it('should be able to get hasThreadControl', async () => { const { kurator } = await setup({ definition: definitionSwitchToHumanActions, }); const context = { getThreadOwner: jest .fn() .mockResolvedValue({ app_id: YOCTOL_AI_APP_ID }), }; const hasThreadControl = kurator.hasThreadControl(context); expect(context.getThreadOwner).toBeCalled(); expect(hasThreadControl).toBeTruthy(); }); it('should support SWITCH_TO_HUMAN action with default YOCTOL.AI app id', async () => { const { middleware, next } = await setup({ definition: definitionSwitchToHumanActions, }); const context = { platform: 'messenger', event: { isText: true, isPayload: false, text: '轉接真人', }, sendButtonTemplate: jest.fn(), passThreadControlToPageInbox: jest.fn(), getThreadOwner: jest .fn() .mockResolvedValue({ app_id: YOCTOL_AI_APP_ID }), }; await middleware(context, next); expect(context.getThreadOwner).toBeCalled(); expect(context.passThreadControlToPageInbox).toBeCalled(); expect(context.sendButtonTemplate).toBeCalledWith( '現在是真人在回應您喔,請耐心等待!', [ { type: 'postback', title: '回到機器人模式', payload: '395936974071928831', }, ], {} ); expect(next).not.toBeCalled(); }); it('should support SWITCH_TO_HUMAN action when receiving request thread control from inbox', async () => { const { middleware, next } = await setup({ definition: definitionSwitchToHumanActions, }); const context = { platform: 'messenger', event: { isRequestThreadControlFromPageInbox: true, isText: false, isPayload: false, }, sendButtonTemplate: jest.fn(), passThreadControlToPageInbox: jest.fn(), getThreadOwner: jest .fn() .mockResolvedValue({ app_id: YOCTOL_AI_APP_ID }), }; await middleware(context, next); expect(context.getThreadOwner).toBeCalled(); expect(context.passThreadControlToPageInbox).toBeCalled(); expect(context.sendButtonTemplate).toBeCalledWith( '現在是真人在回應您喔,請耐心等待!', [ { type: 'postback', title: '回到機器人模式', payload: '395936974071928831', }, ], {} ); expect(next).not.toBeCalled(); }); it('should call ALREADY_SWITCH_TO_HUMAN when trigger SWITCH_TO_HUMAN without haveing thread control', async () => { const { middleware, next } = await setup({ definition: definitionSwitchToHumanActions, }); const context = { platform: 'messenger', event: { isText: true, isPayload: false, text: '轉接真人', }, sendButtonTemplate: jest.fn(), passThreadControlToPageInbox: jest.fn(), getThreadOwner: jest.fn().mockResolvedValue({ app_id: 'OTHER_APP_ID' }), }; await middleware(context, next); expect(context.getThreadOwner).toBeCalled(); expect(context.passThreadControlToPageInbox).not.toBeCalled(); expect(context.sendButtonTemplate).toBeCalledWith( '已經在專人線上回覆模式囉!欲切換回機器人模式,請點選按鈕。', [ { type: 'postback', title: '回到機器人模式', payload: '395936974071928899', }, ], {} ); expect(next).not.toBeCalled(); }); it('should support SWITCH_TO_BOT action with default YOCTOL.AI app id', async () => { const { middleware, next } = await setup({ definition: definitionSwitchToHumanActions, }); const context = { platform: 'messenger', event: { isText: false, isPayload: true, payload: '395936974071928899', }, sendText: jest.fn(), takeThreadControl: jest.fn(), getThreadOwner: jest.fn().mockResolvedValue({ app_id: 'OTHER_APP_ID' }), }; await middleware(context, next); expect(context.getThreadOwner).toBeCalled(); expect(context.takeThreadControl).toBeCalled(); expect(context.sendText).toBeCalledWith('即將為您重啟機器人模式。', {}); expect(next).not.toBeCalled(); }); it('should support ALREADY_SWITCH_TO_BOT action with default YOCTOL.AI app id', async () => { const { middleware, next } = await setup({ definition: definitionSwitchToHumanActions, }); const context = { platform: 'messenger', event: { isPassThreadControl: true, isText: false, isPayload: false, }, sendText: jest.fn(), getThreadOwner: jest.fn().mockResolvedValue({ app_id: 'OTHER_APP_ID' }), }; await middleware(context, next); expect(context.sendText).toBeCalledWith( '真人小編已離開,現在換機器人小編上場!', {} ); expect(next).not.toBeCalled(); }); }); }); describe('#customAdapter', () => { it('should support customAdapter', async () => { const customAdapter = { toTextAction: () => context => context.sendText('custom text'), toImageAction: () => context => context.sendText('custom image'), }; const { middleware, next } = await setup({ customAdapter, definition: { version: '0.5.52', actions: { '1': { id: '1', projectId: '208304842723038640', name: '測試', createdAt: '2018-06-13T09:44:44.114Z', updatedAt: '2018-07-10T03:56:13.115Z', deletedAt: null, triggers: [], actions: [ { id: '209644852630001119', order: 1, type: 'text', descriptor: { text: '基本的', buttons: [], }, actionableId: '208304941675058610', actionableType: 'composite_actions', platform: 'universal', createdAt: '2018-06-15T06:06:53.940Z', updatedAt: '2018-07-10T03:56:13.100Z', deletedAt: null, }, ], }, }, }, }); const context = { platform: 'universal', event: { isText: false, isPayload: true, payload: '1', }, sendText: jest.fn(), }; await middleware(context, next); expect(context.sendText).toBeCalledWith('custom text'); expect(next).not.toBeCalled(); }); }); describe('#chatbase', () => { it('should send message to chatbase when trigger with regex', async () => { const chatbase = require('@google/chatbase'); const userMessage = { setPlatform: jest.fn().mockReturnThis(), setAsTypeUser: jest.fn().mockReturnThis(), setUserId: jest.fn().mockReturnThis(), setIntent: jest.fn().mockReturnThis(), setMessage: jest.fn().mockReturnThis(), setAsHandled: jest.fn().mockReturnThis(), setTimestamp: jest.fn().mockReturnThis(), send: jest.fn().mockResolvedValue(), }; const agentMessage = { setPlatform: jest.fn().mockReturnThis(), setAsTypeAgent: jest.fn().mockReturnThis(), setUserId: jest.fn().mockReturnThis(), setMessage: jest.fn().mockReturnThis(), setTimestamp: jest.fn().mockReturnThis(), send: jest.fn().mockResolvedValue(), }; chatbase.newMessage = jest .fn() .mockReturnValueOnce(userMessage) .mockReturnValueOnce(agentMessage); const { middleware, next } = await setup({ chatbase: { apiKey: '<API_KEY>' }, }); const context = { platform: 'messenger', session: { user: { id: '1234567890', }, }, event: { isText: true, isPayload: false, text: 'AbC', }, sendText: jest.fn(), }; await middleware(context, next); expect(chatbase.newMessage).toBeCalledWith('<API_KEY>'); expect(userMessage.setAsTypeUser).toBeCalled(); expect(userMessage.setPlatform).toBeCalledWith('messenger'); expect(userMessage.setUserId).toBeCalledWith('1234567890'); expect(userMessage.setIntent).toBeCalledWith('測試'); expect(userMessage.setMessage).toBeCalledWith('AbC'); expect(userMessage.setAsHandled).toBeCalled(); expect(userMessage.setTimestamp).toBeCalledWith(expect.any(String)); expect(userMessage.send).toBeCalled(); expect(agentMessage.setAsTypeAgent).toBeCalled(); expect(agentMessage.setPlatform).toBeCalledWith('messenger'); expect(agentMessage.setUserId).toBeCalledWith('1234567890'); expect(agentMessage.setMessage).toBeCalledWith('測試'); expect(agentMessage.setTimestamp).toBeCalledWith(expect.any(String)); expect(agentMessage.send).toBeCalled(); }); it('should send message to chatbase when trigger with payload', async () => { const chatbase = require('@google/chatbase'); const userMessage = { setPlatform: jest.fn().mockReturnThis(), setAsTypeUser: jest.fn().mockReturnThis(), setUserId: jest.fn().mockReturnThis(), setIntent: jest.fn().mockReturnThis(), setMessage: jest.fn().mockReturnThis(), setAsHandled: jest.fn().mockReturnThis(), setTimestamp: jest.fn().mockReturnThis(), send: jest.fn().mockResolvedValue(), }; const agentMessage = { setPlatform: jest.fn().mockReturnThis(), setAsTypeAgent: jest.fn().mockReturnThis(), setUserId: jest.fn().mockReturnThis(), setMessage: jest.fn().mockReturnThis(), setTimestamp: jest.fn().mockReturnThis(), send: jest.fn().mockResolvedValue(), }; chatbase.newMessage = jest .fn() .mockReturnValueOnce(userMessage) .mockReturnValueOnce(agentMessage); const { middleware, next } = await setup({ chatbase: { apiKey: '<API_KEY>' }, }); const context = { platform: 'messenger', session: { user: { id: '1234567890', }, }, event: { isText: false, isPayload: true, payload: '1', }, sendText: jest.fn(), }; await middleware(context, next); expect(chatbase.newMessage).toBeCalledWith('<API_KEY>'); expect(userMessage.setAsTypeUser).toBeCalled(); expect(userMessage.setPlatform).toBeCalledWith('messenger'); expect(userMessage.setUserId).toBeCalledWith('1234567890'); expect(userMessage.setIntent).toBeCalledWith('測試'); expect(userMessage.setMessage).toBeCalledWith('1'); expect(userMessage.setAsHandled).toBeCalled(); expect(userMessage.setTimestamp).toBeCalledWith(expect.any(String)); expect(userMessage.send).toBeCalled(); expect(agentMessage.setAsTypeAgent).toBeCalled(); expect(agentMessage.setPlatform).toBeCalledWith('messenger'); expect(agentMessage.setUserId).toBeCalledWith('1234567890'); expect(agentMessage.setMessage).toBeCalledWith('測試'); expect(agentMessage.setTimestamp).toBeCalledWith(expect.any(String)); expect(agentMessage.send).toBeCalled(); }); it('should not throw when no session user', async () => { const chatbase = require('@google/chatbase'); const userMessage = { setPlatform: jest.fn().mockReturn