@yoctol/kurator
Version:
1,788 lines (1,556 loc) • 65.4 kB
JavaScript
/* 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