From 29e2f644635df9678a286f509a117de8e61e9969 Mon Sep 17 00:00:00 2001 From: Oren Montano Date: Wed, 13 Apr 2022 10:53:54 -0500 Subject: [PATCH 01/10] adding tests --- lambda/test/conversation/billing.test.js | 405 +++++++++++++++++ .../conversation/estimatedRestoration.test.js | 428 ++++++++++++++++++ lambda/test/conversation/reportOutage.test.js | 130 ++++++ lambda/test/util/mockConversationSet.js | 125 +++++ lambda/test/util/mockHandlerInput.js | 58 +++ .../test/util/mockSessionAttributesService.js | 15 + 6 files changed, 1161 insertions(+) create mode 100644 lambda/test/conversation/billing.test.js create mode 100644 lambda/test/conversation/estimatedRestoration.test.js create mode 100644 lambda/test/conversation/reportOutage.test.js create mode 100644 lambda/test/util/mockConversationSet.js create mode 100644 lambda/test/util/mockHandlerInput.js create mode 100644 lambda/test/util/mockSessionAttributesService.js diff --git a/lambda/test/conversation/billing.test.js b/lambda/test/conversation/billing.test.js new file mode 100644 index 0000000..c8b0e4c --- /dev/null +++ b/lambda/test/conversation/billing.test.js @@ -0,0 +1,405 @@ + +const { mockGetSession, mockSaveSession } = require('../util/mockSessionAttributesService') +const { billing } = require('../../conversation/billing') +const { fakePhoneNumber, fakeWebsite } = require('../constants') +const { handlerInput, setSlots } = require('../util/mockHandlerInput') + const { StateHandler } = require('../../conversation'); + +const { + defaultConversationAttributes, + defaultSessionAttributes, + defaultParams, + defaultYesNoIntent, + emptyIntent +} = require('./subConversations/defaults') + +const dialog = jest.fn(); + +const defaultBillingMachineContext = () => ({ + conversationAttributes:defaultConversationAttributes(), + billAmount:"", + misunderstandingCount:0, + error:"", + previousMachineState: "fresh", + resuming: false +}) + +const defaultBillingConversation = () => ({ + billing:{ + machineState:"confirmAddress", + machineContext:defaultBillingMachineContext() + } +}) + +describe('Billing Conversation Tests', () => { + describe('acceptIntent()', () => { + it('Sends machine to confirmAddress when new', async () => { + const params = {...defaultParams(), intent: emptyIntent('Billing')}; + + const { + conversationStack, + currentSubConversation, + sessionAttributes, + fallThrough, + pop, + } = await billing.acceptIntent(params); + + expect(conversationStack).toEqual([ + defaultBillingConversation() + ]); + expect(currentSubConversation).toEqual( + { + "confirmAddress":{} + } + ); + expect(sessionAttributes).toEqual(defaultSessionAttributes()); + expect(fallThrough).toBeFalsy(); + expect(pop).toBeFalsy(); + }) + + it('Sends machine to correctAddress on correctAddress:true', async () => { + const params = {...defaultParams(), + currentSubConversation: { + billing: {...defaultBillingConversation().billing, + machineContext: { + ...defaultBillingMachineContext(), + conversationAttributes: defaultConversationAttributes(true, true), + previousMachineState: "confirmAddress" + } + } + }, + sessionAttributes: {...defaultSessionAttributes(), + conversationAttributes: defaultConversationAttributes(true, true) + }, + intent: defaultYesNoIntent(), + poppedConversation:true + }; + + const { + conversationStack, + currentSubConversation, + sessionAttributes, + fallThrough, + pop, + } = await billing.acceptIntent(params); + + expect(conversationStack).toEqual([]); + expect(currentSubConversation).toEqual( + { + billing:{ ...defaultBillingConversation().billing, + machineState:"reportBillToUser", + machineContext: {...defaultBillingMachineContext(), + conversationAttributes: defaultConversationAttributes(true, true), + billAmount:currentSubConversation.billing.machineContext.billAmount, + previousMachineState:"confirmAddress", + resuming: true + } + } + } + ); + expect(sessionAttributes).toEqual({...defaultSessionAttributes(), + conversationAttributes:defaultConversationAttributes(true, true) + }); + expect(fallThrough).toBeFalsy(); + expect(pop).toBeTruthy(); + }) + + it('Sends machine to incorrectAddress on correctAddress:false', async () => { + const params = {...defaultParams(), + currentSubConversation: defaultBillingConversation(), + poppedConversation: true + }; + + const { + conversationStack, + currentSubConversation, + sessionAttributes, + fallThrough, + pop, + } = await billing.acceptIntent(params); + + expect(conversationStack).toEqual([]); + expect(currentSubConversation).toEqual( + { + billing:{ + ...defaultBillingConversation().billing, + machineState:"incorrectAddress", + machineContext:{ + ...defaultBillingMachineContext(), + previousMachineState: "confirmAddress", + resuming: true + } + } + } + ); + expect(sessionAttributes).toEqual(defaultSessionAttributes()); + expect(fallThrough).toBeFalsy(); + expect(pop).toBeTruthy(); + }) + + it('Sends machine to reportBillToUser confirmedAddress true and correctAddress true', async () => { + const params = { + ...defaultParams(), + currentSubConversation: { + billing:{} + }, + sessionAttributes: { + ...defaultSessionAttributes(), + conversationAttributes:defaultConversationAttributes(true, true) + }, + intent: { + name:'test', + slots: {} + } + }; + + const { + conversationStack, + currentSubConversation, + sessionAttributes, + fallThrough, + pop, + } = await billing.acceptIntent(params); + + expect(conversationStack).toEqual([]); + expect(currentSubConversation).toEqual( + { + billing:{ + ...defaultBillingConversation().billing, + machineState:"reportBillToUser", + machineContext:{ + ...defaultBillingMachineContext(), + conversationAttributes:defaultConversationAttributes(true, true), + billAmount:currentSubConversation.billing.machineContext.billAmount, + previousMachineState: "fresh", + resuming: false + } + } + } + ); + expect(sessionAttributes).toEqual({ + ...defaultSessionAttributes(), + conversationAttributes:defaultConversationAttributes(true, true) + }); + expect(fallThrough).toBeFalsy(); + expect(pop).toBeTruthy(); + }); + + it('Sends machine to reportBillToUser confirmedAddress true and correctAddress true', async () => { + const params = { + ...defaultParams(), + currentSubConversation: { + billing:{} + }, + sessionAttributes: { + ...defaultSessionAttributes(), + conversationAttributes:defaultConversationAttributes(true, false) + }, + intent: { + name:'test', + slots: {} + } + }; + + const { + conversationStack, + currentSubConversation, + sessionAttributes, + fallThrough, + pop, + } = await billing.acceptIntent(params); + + expect(conversationStack).toEqual([]); + expect(currentSubConversation).toEqual( + { + billing:{ + ...defaultBillingConversation().billing, + machineState:"incorrectAddress", + machineContext:{ + ...defaultBillingMachineContext(), + conversationAttributes:defaultConversationAttributes(true, false), + billAmount:currentSubConversation.billing.machineContext.billAmount, + previousMachineState: "fresh", + resuming: false + } + } + } + ); + expect(sessionAttributes).toEqual({ + ...defaultSessionAttributes(), + conversationAttributes:defaultConversationAttributes(true, false) + }); + expect(fallThrough).toBeFalsy(); + expect(pop).toBeTruthy(); + }); + }); + + describe('craftResponse()', () => { + beforeEach(() => { + dialog.mockRestore(); + mockGetSession.mockClear() + mockSaveSession.mockClear() + }); + + it('when address is incorrect, tells the user they must correct their address online or by phone to get an accurate bill report (badAddress)', () => { + const params = { + dialog, + subConversation: { + billing: { + machineState: 'incorrectAddress', + machineContext: {}, + }, + }, + }; + + billing.craftResponse(params); + + expect(dialog.mock.calls[0][0]).toEqual('billing.wrongAddress'); + expect(dialog.mock.calls[0][1]).toEqual({ website: fakeWebsite, phoneNumber: fakePhoneNumber }); + }); + + it('when address is correct, tells the user their bill (reportBillToUser)', () => { + const params = { + dialog, + subConversation: { + billing: { + machineState: 'reportBillToUser', + machineContext: { billAmount: 'fake bill amount' }, + }, + }, + }; + + billing.craftResponse(params); + + expect(dialog.mock.calls[0][0]).toEqual('billing.returnBill'); + expect(dialog.mock.calls[0][1]).toEqual({ billAmount: 'fake bill amount' }); + }); + + it('gives the generic error response on errors (error)', () => { + const params = { + dialog, + subConversation: { + billing: { + machineState: 'error', + machineContext: { error: 'fake error' }, + }, + }, + }; + + billing.craftResponse(params); + + expect(dialog.mock.calls[0][0]).toEqual('home.error'); + }); + }); + + describe('systemic test', () => { + beforeEach(() => { + dialog.mockRestore(); + handlerInput.requestEnvelope.request.type = 'IntentRequest'; + handlerInput.responseBuilder.speak.mockClear() + handlerInput.responseBuilder.reprompt.mockClear() + handlerInput.responseBuilder.addElicitSlotDirective.mockClear() + handlerInput.responseBuilder.addConfirmSlotDirective.mockClear() + handlerInput.responseBuilder.withShouldEndSession.mockClear() + handlerInput.responseBuilder.getResponse.mockClear() + mockGetSession.mockClear() + mockSaveSession.mockClear() + }); + + it('walks through triggering uniqueness', async () => { + const requestAttributes = { + 'confirmAddress.confirm': "confirmAddress.confirm", + "confirmAddress.misheard": "confirmAddress.misheard", + "estimatedRestoration.homeOrOther.confirm": "estimatedRestoration.homeOrOther.confirm", + "estimatedRestoration.homeOrOther.misheard": "estimatedRestoration.homeOrOther.misheard", + "confirmAddress.resume": "confirmAddress.resume", + "billing.returnBill": "billing.returnBill", + "estimatedRestoration.reply.home": "estimatedRestoration.reply.home", + "home.reEngage": "home.reEngage", + } + handlerInput.attributesManager.setRequestAttributes(requestAttributes) + handlerInput.attributesManager.setSessionAttributes({ + ...defaultSessionAttributes, + conversationAttributes:{ + ...defaultConversationAttributes(false, false), + }, + state: { + currentSubConversation: { + engagement: {} + }, + conversationStack: [ + ], + } + }) + + const slots = {} + setSlots(slots) + + handlerInput.requestEnvelope.request.intent.name = 'Billing' + await StateHandler.handle(handlerInput) + + expect(handlerInput.responseBuilder.speak.mock.calls.length).toEqual(1) + expect(handlerInput.responseBuilder.speak.mock.calls[0][0]).toEqual('confirmAddress.confirm'); + handlerInput.responseBuilder.speak.mockClear(); + + handlerInput.requestEnvelope.request.intent.name = 'EstimatedRestoration' + await StateHandler.handle(handlerInput) + + expect(handlerInput.responseBuilder.speak.mock.calls.length).toEqual(1) + expect(handlerInput.responseBuilder.speak.mock.calls[0][0]).toEqual('estimatedRestoration.homeOrOther.confirm'); + handlerInput.responseBuilder.speak.mockClear(); + + handlerInput.requestEnvelope.request.intent.name = 'Billing' + await StateHandler.handle(handlerInput) + + expect(handlerInput.responseBuilder.speak.mock.calls.length).toEqual(1) + expect(handlerInput.responseBuilder.speak.mock.calls[0][0]).toEqual('confirmAddress.misheard'); + handlerInput.responseBuilder.speak.mockClear(); + + handlerInput.requestEnvelope.request.intent.name = 'EstimatedRestoration' + await StateHandler.handle(handlerInput) + + expect(handlerInput.responseBuilder.speak.mock.calls.length).toEqual(1) + expect(handlerInput.responseBuilder.speak.mock.calls[0][0]).toEqual('estimatedRestoration.homeOrOther.misheard'); + handlerInput.responseBuilder.speak.mockClear(); + + handlerInput.requestEnvelope.request.intent.name = 'HomeIntent' + await StateHandler.handle(handlerInput) + + expect(handlerInput.responseBuilder.speak.mock.calls.length).toEqual(1) + expect(handlerInput.responseBuilder.speak.mock.calls[0][0]).toEqual('confirmAddress.confirm'); + handlerInput.responseBuilder.speak.mockClear(); + + handlerInput.requestEnvelope.request.intent.name = 'Billing' + await StateHandler.handle(handlerInput) + + expect(handlerInput.responseBuilder.speak.mock.calls.length).toEqual(1) + expect(handlerInput.responseBuilder.speak.mock.calls[0][0]).toEqual('confirmAddress.misheard'); + handlerInput.responseBuilder.speak.mockClear(); + + handlerInput.requestEnvelope.request.intent.name = 'YesNoIntent' + setSlots(defaultYesNoIntent('yes').slots); + await StateHandler.handle(handlerInput) + + expect(handlerInput.responseBuilder.speak.mock.calls.length).toEqual(1) + expect(handlerInput.responseBuilder.speak.mock.calls[0][0]).toEqual('billing.returnBill estimatedRestoration.reply.home home.reEngage'); + handlerInput.responseBuilder.speak.mockClear(); + + //should be in home or other + + const { state } = handlerInput.attributesManager.getSessionAttributes() + + expect(state.conversationStack).toEqual([]) + expect(state.currentSubConversation).toEqual({ + engagement: { + machineState: 'resume', + machineContext: { + resuming: true, + previousMachineState: "billing", + conversationAttributes: defaultConversationAttributes(true, true) + } + } + }) + }); + }); +}) diff --git a/lambda/test/conversation/estimatedRestoration.test.js b/lambda/test/conversation/estimatedRestoration.test.js new file mode 100644 index 0000000..4a920e3 --- /dev/null +++ b/lambda/test/conversation/estimatedRestoration.test.js @@ -0,0 +1,428 @@ +const { mockGetSession, mockSaveSession } = require('../util/mockSessionAttributesService') +const { estimatedRestoration } = require('../../conversation/estimatedRestoration') +const { fakePhoneNumber, fakeWebsite } = require('../constants') +const { handlerInput, setSlots } = require('../util/mockHandlerInput') +const { StateHandler } = require('../../../../conversation'); +const { defaultSessionAttributes, defaultConversationAttributes, defaultYesNoIntent, defaultPickALetterIntent } = require('../defaults'); + +const dialog = jest.fn(); + +const defaultEstimatedRestorationMachineContext = () => ({ + selectedHome: false, + selectedOutage: '', + estimate: '', + misunderstandingCount: 0, + error: '', + previousMachineState: 'fresh', + resuming: false, + conversationAttributes: defaultConversationAttributes(), + address: "11477 Olde Cabin Rd Suite 320" +}) + +describe('Estimated Restoration Conversation Tests', () => { + describe('acceptIntent()', () => { + it('Sends machine to askAboutAddress when new', async () => { + const params = { + conversationStack: [], + currentSubConversation: { estimatedRestoration:{}}, + sessionAttributes: {}, + intent: { + name: 'EstimatedRestoration', + slots: {}, + }, + topConversation: true, + newConversation: false, + poppedConversation: false, + fallThrough: false, + }; + + const { + conversationStack, + currentSubConversation, + sessionAttributes, + fallThrough, + pop, + } = await estimatedRestoration.acceptIntent(params); + + expect(conversationStack).toEqual([]); + expect(currentSubConversation).toEqual({ + estimatedRestoration:{ + machineState: 'askAboutHomeOrOther', + machineContext: { + ...defaultEstimatedRestorationMachineContext(), + conversationAttributes: {}, + }, + }, + }); + expect(sessionAttributes).toEqual({ conversationAttributes: {}}); + expect(fallThrough).toBeFalsy(); + expect(pop).toBeFalsy(); + }) + + it('Sends machine from askAboutHomeOrOther to selectOtherOutage when user triggers OtherIntent', async () => { + const params = { + conversationStack: [{ + engagement: { + machineState: 'estimatedRestoration', + machineContext: {} + } + }], + currentSubConversation: { + estimatedRestoration: { + machineState: 'askAboutHomeOrOther', + machineContext: { + ...defaultEstimatedRestorationMachineContext(), + conversationAttributes: {}, + }, + }, + }, + sessionAttributes: {}, + intent: { + name: 'OtherIntent', + slots: {}, + }, + topConversation: true, + newConversation: false, + poppedConversation: false, + fallThrough: false, + }; + + const { + conversationStack, + currentSubConversation, + sessionAttributes, + fallThrough, + pop, + } = await estimatedRestoration.acceptIntent(params); + + expect(conversationStack).toEqual(params.conversationStack); + expect(currentSubConversation).toEqual({ + pickAnOutage:{} + }); + expect(sessionAttributes).toEqual({ conversationAttributes: {}}); + expect(fallThrough).toBeFalsy(); + expect(pop).toBeFalsy(); + }) + }); + + describe('craftResponse()', () => { + beforeEach(() => { + dialog.mockRestore(); + mockGetSession.mockClear() + mockSaveSession.mockClear() + }); + + it('when address is incorrect, tells the user they must correct their address online or by phone to get a restoration estimate (badAddress)', () => { + const params = { + dialog, + subConversation: { + estimatedRestoration: { + machineState: 'incorrectAddress', + machineContext: {}, + }, + }, + sessionAttributes: { + previousPoppedConversation:'' + } + }; + + estimatedRestoration.craftResponse(params); + + expect(dialog.mock.calls[0][0]).toEqual('estimatedRestoration.address.wrongAddress'); + expect(dialog.mock.calls[0][1]).toEqual({ website: fakeWebsite, phoneNumber: fakePhoneNumber }); + }); + + it('when address is correct, gives the user their estimated restoration time (reportEstimateToUser)', () => { + const params = { + dialog, + subConversation: { + estimatedRestoration: { + machineState: 'reportEstimateToUser', + machineContext: { estimate: '6 hours' }, + }, + }, + sessionAttributes: { + previousPoppedConversation:'' + } + }; + + estimatedRestoration.craftResponse(params); + + expect(dialog.mock.calls[0][0]).toEqual('estimatedRestoration.reply.other'); + expect(dialog.mock.calls[0][1]).toEqual({ estimate: '6 hours' }); + }); + + it('gives the generic error response on errors (error)', () => { + const params = { + dialog, + subConversation: { + estimatedRestoration: { + machineState: 'error', + machineContext: { error: 'fake error' }, + }, + }, + sessionAttributes: { + previousPoppedConversation:'' + } + }; + + estimatedRestoration.craftResponse(params); + + expect(dialog.mock.calls[0][0]).toEqual('home.error'); + }); + }); + + describe('systemic test', () => { + beforeEach(() => { + dialog.mockRestore(); + handlerInput.requestEnvelope.request.type = 'IntentRequest'; + handlerInput.responseBuilder.speak.mockClear() + handlerInput.responseBuilder.reprompt.mockClear() + handlerInput.responseBuilder.addElicitSlotDirective.mockClear() + handlerInput.responseBuilder.addConfirmSlotDirective.mockClear() + handlerInput.responseBuilder.withShouldEndSession.mockClear() + handlerInput.responseBuilder.getResponse.mockClear() + mockGetSession.mockClear() + mockSaveSession.mockClear() + }); + + it('sends the machine to correctAddress after confirming address when user is asking about their home', async () => { + const requestAttributes = { + 'estimatedRestoration.reply.other': 'estimatedRestoration.reply.other' + } + handlerInput.attributesManager.setRequestAttributes(requestAttributes) + + handlerInput.attributesManager.setRequestAttributes(requestAttributes) + handlerInput.attributesManager.setSessionAttributes({ + ...defaultSessionAttributes, + conversationAttributes:defaultConversationAttributes(true, true), + state: { + currentSubConversation: { + confirmAddress: { + machineState: 'yesAnswer', + machineContext: { + address: "11477 Olde Cabin Rd Suite 320", + misunderstandingCount: 0, + previousMachineState: "confirmAddress", + resuming: false, + conversationAttributes: { + correctAddress: true, + confirmedAddress: true + } + } + } + }, + conversationStack: [{ + estimatedRestoration: { + machineState: "confirmAddress", + machineContext: defaultEstimatedRestorationMachineContext(), + } + }], + } + }) + + const slots1 = {} + setSlots(slots1) + + handlerInput.requestEnvelope.request.intent.name = 'HomeIntent' + + await StateHandler.handle(handlerInput) + + const { state } = handlerInput.attributesManager.getSessionAttributes() + + expect(state.currentSubConversation).toEqual({ + estimatedRestoration: { + machineState: 'reportEstimateToUser', + machineContext: { + ...defaultEstimatedRestorationMachineContext(), + estimate: '3 hours', + resuming: true, + previousMachineState: 'confirmAddress', + conversationAttributes: { + confirmAddress: { + correctAddress: true, + confirmedAddress: true, + }, + resume: { + wipeConversation: false, + }, + }, + }, + }, + }) + expect(handlerInput.responseBuilder.speak.mock.calls.length).toEqual(1) + }); + + it('sends the machine to incorrectAddress after confirming address when user is asking about their home', async () => { + const requestAttributes = { + 'estimatedRestoration.address.wrongAddress': 'estimatedRestoration.address.wrongAddress' + } + handlerInput.attributesManager.setRequestAttributes(requestAttributes) + + handlerInput.attributesManager.setRequestAttributes(requestAttributes) + handlerInput.attributesManager.setSessionAttributes({ + ...defaultSessionAttributes, + conversationAttributes:defaultConversationAttributes(true, false), + state: { + currentSubConversation: { + confirmAddress: { + machineState: 'noAnswer', + machineContext: { + address: "11477 Olde Cabin Rd Suite 320", + misunderstandingCount: 0, + previousMachineState: "confirmAddress", + resuming: false, + conversationAttributes: { + correctAddress: false, + confirmedAddress: true + } + } + } + }, + conversationStack: [ + { + estimatedRestoration: { + machineState: "confirmAddress", + machineContext: { + ...defaultEstimatedRestorationMachineContext() + } + } + } + ], + } + }) + + const slots1 = {} + setSlots(slots1) + + handlerInput.requestEnvelope.request.intent.name = 'HomeIntent' + + await StateHandler.handle(handlerInput) + + const { state } = handlerInput.attributesManager.getSessionAttributes() + + expect(state.currentSubConversation).toEqual({ + estimatedRestoration: { + machineState: 'incorrectAddress', + machineContext: { + ...defaultEstimatedRestorationMachineContext(), + resuming: true, + previousMachineState: 'confirmAddress', + conversationAttributes: defaultConversationAttributes(true), + } + } + }) + expect(handlerInput.responseBuilder.speak.mock.calls.length).toEqual(1) + }); + + it('walks through the expected path of estimatedRestoration', async () => { + const requestAttributes = { + 'estimatedRestoration.reply.other': 'estimatedRestoration.reply.other', + 'estimatedRestoration.address.wrongAddress': 'estimatedRestoration.address.wrongAddress', + 'estimatedRestoration.homeOrOther.confirm': 'estimatedRestoration.homeOrOther.confirm', + 'confirmAddress.confirm': 'confirmAddress.confirm', + 'estimatedRestoration.reply.home': 'estimatedRestoration.reply.home', + 'home.reEngage': 'home.reEngage', + 'estimatedRestoration.otherLocation.confirm': 'estimatedRestoration.otherLocation.confirm' + } + handlerInput.attributesManager.setRequestAttributes(requestAttributes) + handlerInput.attributesManager.setSessionAttributes({ + ...defaultSessionAttributes, + conversationAttributes:{ + ...defaultConversationAttributes(false, false), + }, + state: { + currentSubConversation: { + engagement: {} + }, + conversationStack: [ + ], + } + }) + + const slots = {} + setSlots(slots) + + handlerInput.requestEnvelope.request.intent.name = 'EstimatedRestoration' + await StateHandler.handle(handlerInput) + + expect(handlerInput.responseBuilder.speak.mock.calls.length).toEqual(1) + expect(handlerInput.responseBuilder.speak.mock.calls[0][0]).toEqual('estimatedRestoration.homeOrOther.confirm'); + handlerInput.responseBuilder.speak.mockClear(); + + handlerInput.requestEnvelope.request.intent.name = 'HomeIntent' + await StateHandler.handle(handlerInput) + + expect(handlerInput.responseBuilder.speak.mock.calls.length).toEqual(1) + expect(handlerInput.responseBuilder.speak.mock.calls[0][0]).toEqual('confirmAddress.confirm'); + handlerInput.responseBuilder.speak.mockClear(); + + handlerInput.requestEnvelope.request.intent.name = 'YesNoIntent' + setSlots(defaultYesNoIntent('yes').slots); + await StateHandler.handle(handlerInput) + + expect(handlerInput.responseBuilder.speak.mock.calls.length).toEqual(1) + expect(handlerInput.responseBuilder.speak.mock.calls[0][0]).toEqual('estimatedRestoration.reply.home home.reEngage'); + handlerInput.responseBuilder.speak.mockClear(); + + handlerInput.requestEnvelope.request.intent.name = 'EstimatedRestoration' + setSlots(slots) + await StateHandler.handle(handlerInput) + + expect(handlerInput.responseBuilder.speak.mock.calls.length).toEqual(1) + expect(handlerInput.responseBuilder.speak.mock.calls[0][0]).toEqual('estimatedRestoration.homeOrOther.confirm'); + handlerInput.responseBuilder.speak.mockClear(); + + handlerInput.requestEnvelope.request.intent.name = 'HomeIntent' + await StateHandler.handle(handlerInput) + + expect(handlerInput.responseBuilder.speak.mock.calls.length).toEqual(1) + expect(handlerInput.responseBuilder.speak.mock.calls[0][0]).toEqual('estimatedRestoration.reply.home home.reEngage'); + handlerInput.responseBuilder.speak.mockClear(); + + handlerInput.requestEnvelope.request.intent.name = 'EstimatedRestoration' + await StateHandler.handle(handlerInput) + + expect(handlerInput.responseBuilder.speak.mock.calls.length).toEqual(1) + expect(handlerInput.responseBuilder.speak.mock.calls[0][0]).toEqual('estimatedRestoration.homeOrOther.confirm'); + handlerInput.responseBuilder.speak.mockClear(); + + handlerInput.requestEnvelope.request.intent.name = 'OtherIntent' + await StateHandler.handle(handlerInput) + + expect(handlerInput.responseBuilder.speak.mock.calls.length).toEqual(1) + expect(handlerInput.responseBuilder.speak.mock.calls[0][0]).toEqual('estimatedRestoration.otherLocation.confirm'); + handlerInput.responseBuilder.speak.mockClear(); + + handlerInput.requestEnvelope.request.intent.name = 'PickALetterIntent' + setSlots(defaultPickALetterIntent().slots) + await StateHandler.handle(handlerInput) + + expect(handlerInput.responseBuilder.speak.mock.calls.length).toEqual(1) + expect(handlerInput.responseBuilder.speak.mock.calls[0][0]).toEqual('estimatedRestoration.reply.other home.reEngage'); + handlerInput.responseBuilder.speak.mockClear(); + + const { state } = handlerInput.attributesManager.getSessionAttributes() + + expect(state.currentSubConversation).toEqual({ + engagement: { + machineState: 'resume', + machineContext: { + resuming: true, + previousMachineState: "estimatedRestoration", + conversationAttributes: { + confirmAddress: { + correctAddress: true, + confirmedAddress: true, + }, + resume: { + wipeConversation: false, + } + }, + }, + }, + }) + }); + }); +}); + diff --git a/lambda/test/conversation/reportOutage.test.js b/lambda/test/conversation/reportOutage.test.js new file mode 100644 index 0000000..761616e --- /dev/null +++ b/lambda/test/conversation/reportOutage.test.js @@ -0,0 +1,130 @@ + +const { reportOutage } = require('../../conversation/reportOutage') +const { fakePhoneNumber, fakeWebsite } = require('../../constants') + +const dialog = jest.fn(); + +describe('Report Outage Conversation Tests', () => { + describe('acceptIntent()', () => { + it('Sends machine to askAboutAddress when new', async () => { + const params = { + conversationStack: [], + currentSubConversation: {reportOutage:{}}, + sessionAttributes: { + previousPoppedConversation: '' + }, + intent: { + name:'ReportOutage', + slots: {}, + }, + topConversation: true, + newConversation: false, + poppedConversation: false, + fallThrough: false, + }; + + const { + conversationStack, + currentSubConversation, + sessionAttributes, + fallThrough, + pop, + } = await reportOutage.acceptIntent(params); + + expect(conversationStack).toEqual([ + { + reportOutage:{ + machineState:"confirmAddress", + machineContext:{ + conversationAttributes:{}, + letThemKnow:"", + misunderstandingCount:0, + error:"", + previousMachineState: "fresh", + resuming: false + } + } + } + ]); + expect(currentSubConversation).toEqual( + { + confirmAddress: {} + } + ); + expect(sessionAttributes).toEqual( + { + conversationAttributes:{}, + previousPoppedConversation:'' + } + ); + expect(fallThrough).toBeFalsy(); + expect(pop).toBeFalsy(); + }) + + + }); + + describe('craftResponse()', () => { + beforeEach(() => { + dialog.mockRestore(); + }); + + it('when address is incorrect, tells the user they must correct their address online or by phone to report outage (badAddress)', () => { + const params = { + dialog, + subConversation: { + reportOutage: { + machineState: 'incorrectAddress', + machineContext: {}, + }, + }, + sessionAttributes: { + previousPoppedConversation: '' + } + }; + + reportOutage.craftResponse(params); + + expect(dialog.mock.calls[0][0]).toEqual('reportOutage.wrongAddress'); + expect(dialog.mock.calls[0][1]).toEqual({ website: fakeWebsite, phoneNumber: fakePhoneNumber }); + }); + + it('when address is correct, tells the user thanks for reporting(thanksForReporting)', () => { + const params = { + dialog, + subConversation: { + reportOutage: { + machineState: 'thanksForReporting', + machineContext: {}, + }, + }, + sessionAttributes: { + previousPoppedConversation: '' + } + }; + + reportOutage.craftResponse(params); + + expect(dialog.mock.calls[0][0]).toEqual('reportOutage.reply.noContact'); + }); + + it('gives the generic error response on errors (error)', () => { + const params = { + dialog, + subConversation: { + reportOutage: { + machineState: 'error', + machineContext: { error: 'fake error' }, + }, + }, + sessionAttributes: { + previousPoppedConversation: '' + } + }; + + reportOutage.craftResponse(params); + + expect(dialog.mock.calls[0][0]).toEqual('home.error'); + }); + }); +}); diff --git a/lambda/test/util/mockConversationSet.js b/lambda/test/util/mockConversationSet.js new file mode 100644 index 0000000..08b1097 --- /dev/null +++ b/lambda/test/util/mockConversationSet.js @@ -0,0 +1,125 @@ +const conversationSet = { + engagement: { + craftResponse: ({ finalWords }) => { + if ( finalWords ) { + return ''; + } + return "engagement text"; + }, + acceptIntent: async ({ conversationStack, currentSubConversation, sessionAttributes, intent, topConversation, poppedConversation, fallThrough }) => { + if (!fallThrough) { + if (topConversation) { + if (!poppedConversation) { + switch (intent.name) { + case 'step1Intent' : { + conversationStack.push({engagement:{}}); + currentSubConversation = {step1: {}}; + return {conversationStack, currentSubConversation, sessionAttributes, fallThrough: false, pop: false}; + } + case 'step2Intent' : { + conversationStack.push({engagement: {}}); + currentSubConversation = {step2: {}}; + return {conversationStack, currentSubConversation, sessionAttributes, fallThrough: false, pop: false}; + } + } + } + } else { + switch (intent.name) { + case 'step2Intent' : { + if (Object.keys(currentSubConversation)[0] != Object.keys({step2: {}})[0] || currentSubConversation.step2.numValue != intent.slots.num.value) { + conversationStack.push(currentSubConversation); + currentSubConversation = {step2: {}}; + return {conversationStack, currentSubConversation, sessionAttributes, fallThrough: false, pop: false}; + } + } + } + } + } + //all accept intents must return these if nothing is changed + return {conversationStack, currentSubConversation, sessionAttributes, fallThrough: false, pop: false}; + }, + }, + step1: { + craftResponse: ({ dialog, conversationStack, subConversation, sessionAttributes, intent, finalWords }) => { + if (subConversation.step1.complain) { + return "you're already in step1"; + } + return "step1 text"; + }, + acceptIntent: async ({ conversationStack, currentSubConversation, sessionAttributes, intent, topConversation, newConversation, poppedConversation, fallThrough }) => { + if (!fallThrough) { + if (topConversation) { + switch (intent.name) { + case 'step1Intent' : { + if (!newConversation) { + currentSubConversation.step1.complain = true; + return {conversationStack, currentSubConversation, sessionAttributes, fallThrough: false, pop: false}; + } + } + } + } + } + return {conversationStack, currentSubConversation, sessionAttributes, fallThrough: false, pop: false}; + }, + }, + step2: { + craftResponse: ({ dialog, conversationStack, subConversation, sessionAttributes, intent, finalWords }) => { + if (subConversation.step2.resumeStatement) { + return "step2 resume"; + } + if (subConversation.step2.numValue && !subConversation.step2.goodValue) { + //confirm numval + return `step2 text ${subConversation.step2.numValue}`; + } else if (subConversation.step2.numValue && intent.slots.good.value === 'yes') { + return "step2 thanks"; + } else { + //ask for numval + return "step2 text"; + } + }, + acceptIntent: async ({ conversationStack, currentSubConversation, sessionAttributes, intent, topConversation, newConversation, poppedConversation, fallThrough }) => { + if (!fallThrough) { + if (topConversation) { + delete currentSubConversation.step2.resumeStatement; + switch(intent.name) { + case 'step2Intent': + if (poppedConversation) { + currentSubConversation.step2.resumeStatement = true; + return {conversationStack, currentSubConversation, sessionAttributes, fallThrough: false, pop: false}; + } + if(intent.slots.num?.value){ + currentSubConversation.step2.numValue = intent.slots.num.value; + } + if(intent.slots.good?.value){ + if (intent.slots.good.value === 'yes') { + //Call an API, pop this conversation while saying something...? + currentSubConversation.step2.goodValue = intent.slots.good.value; + + return {conversationStack, currentSubConversation, sessionAttributes, fallThrough: false, pop: true}; + } else { + delete currentSubConversation.step2.numValue; + delete currentSubConversation.step2.goodValue; + } + } + return {conversationStack, currentSubConversation, sessionAttributes, fallThrough: false, pop: false}; + case 'yesNoIntent': + if (currentSubConversation.step2.numValue) { + if (intent.slots.good.value === 'yes') { + //Call an API, pop this conversation while saying something...? + currentSubConversation.step2.goodValue = intent.slots.yesNo.value; + return {conversationStack, currentSubConversation, sessionAttributes, fallThrough: false, pop: true}; + } else { + delete currentSubConversation.step2.numValue; + delete currentSubConversation.step2.goodValue; + } + } + return {conversationStack, currentSubConversation, sessionAttributes, fallThrough: false, pop: false}; + } + } + } + return {conversationStack, currentSubConversation, sessionAttributes, fallThrough: false, pop: false}; + }, + }, +} + +module.exports = { conversationSet }; diff --git a/lambda/test/util/mockHandlerInput.js b/lambda/test/util/mockHandlerInput.js new file mode 100644 index 0000000..f330ef6 --- /dev/null +++ b/lambda/test/util/mockHandlerInput.js @@ -0,0 +1,58 @@ +const mock_ = require('lodash') + +let mockRequestAttributesKeys = {} +let mockSessionAttributes = {} +let mockSlots = {} + +const setRequestAttributes = newKeys => { + mockRequestAttributesKeys = mock_.merge(mockRequestAttributesKeys, newKeys) +} + +const setSlots = newSlots => { + mockSlots = mock_.merge(mockSlots, newSlots) +} + +const handlerInput = { + attributesManager: { + getRequestAttributes: () => ({ + t: key => { + if (!mockRequestAttributesKeys[key]) console.log(`getRequestAttributes called with missing key: ${key}`); + return mockRequestAttributesKeys[key]; + } + }), + setRequestAttributes, + getSessionAttributes: () => mockSessionAttributes, + setSessionAttributes: newSession => { + mockSessionAttributes = newSession + }, + }, + responseBuilder: { + speak: jest.fn().mockReturnThis(), + withShouldEndSession: jest.fn().mockReturnThis(), + getResponse: jest.fn().mockReturnThis(), + addElicitSlotDirective: jest.fn().mockReturnThis(), + addConfirmSlotDirective: jest.fn().mockReturnThis(), + reprompt: jest.fn().mockReturnThis(), + addDelegateDirective: jest.fn().mockReturnThis(), + withSimpleCard: jest.fn().mockReturnThis(), + }, + requestEnvelope: { + request: { + intent: { + name: 'GenericIntentName', + slots: mockSlots, + }, + type: 'GenericRequestType', + }, + session: { + user: { + userId: 'fakeUserId' + }, + }, + }, +} + +module.exports = { + handlerInput, + setSlots, +} diff --git a/lambda/test/util/mockSessionAttributesService.js b/lambda/test/util/mockSessionAttributesService.js new file mode 100644 index 0000000..7dc4a83 --- /dev/null +++ b/lambda/test/util/mockSessionAttributesService.js @@ -0,0 +1,15 @@ +const { handlerInput } = require('./mockHandlerInput') + +const mockGetSession = jest.fn(() => handlerInput.attributesManager.getSessionAttributes()) +const mockSaveSession = jest.fn(sessionAttributes => handlerInput.attributesManager.setSessionAttributes(sessionAttributes)) + +jest.mock('../../service/SessionAttributesService', () => ({ + __esModule: true, + getSessionFromDynamoDb: mockGetSession, + saveSessionToDynamoDb: mockSaveSession, +})) + +module.exports = { + mockGetSession, + mockSaveSession, +} From 165a40d5d2658395b6b8688598730b87117fcfac Mon Sep 17 00:00:00 2001 From: Lucas Date: Tue, 19 Apr 2022 16:30:58 -0500 Subject: [PATCH 02/10] refactor mocks --- .../conversation/estimatedRestoration.test.js | 155 +++++------------- lambda/test/util/defaults.js | 82 +++++++++ lambda/test/util/mockConversationSet.js | 125 -------------- .../test/util/mockSessionAttributesService.js | 15 -- lambda/test/util/mocks.js | 79 +++++++++ 5 files changed, 205 insertions(+), 251 deletions(-) create mode 100644 lambda/test/util/defaults.js delete mode 100644 lambda/test/util/mockConversationSet.js delete mode 100644 lambda/test/util/mockSessionAttributesService.js create mode 100644 lambda/test/util/mocks.js diff --git a/lambda/test/conversation/estimatedRestoration.test.js b/lambda/test/conversation/estimatedRestoration.test.js index 4a920e3..0b229e8 100644 --- a/lambda/test/conversation/estimatedRestoration.test.js +++ b/lambda/test/conversation/estimatedRestoration.test.js @@ -1,11 +1,16 @@ -const { mockGetSession, mockSaveSession } = require('../util/mockSessionAttributesService') -const { estimatedRestoration } = require('../../conversation/estimatedRestoration') -const { fakePhoneNumber, fakeWebsite } = require('../constants') -const { handlerInput, setSlots } = require('../util/mockHandlerInput') -const { StateHandler } = require('../../../../conversation'); -const { defaultSessionAttributes, defaultConversationAttributes, defaultYesNoIntent, defaultPickALetterIntent } = require('../defaults'); - -const dialog = jest.fn(); +const { defaultSessionAttributes, defaultConversationAttributes, defaultYesNoIntent, defaultPickALetterIntent } = require('../util/defaults'); +const { + StateHandler, + mockGetSession, + mockSaveSession, + getMockState, + handlerInput, + setSlots, + dialog, +} = require('../util/mocks') + +const fakePhoneNumber = '111-111-1111' +const fakeWebsite = 'google.com' const defaultEstimatedRestorationMachineContext = () => ({ selectedHome: false, @@ -20,99 +25,40 @@ const defaultEstimatedRestorationMachineContext = () => ({ }) describe('Estimated Restoration Conversation Tests', () => { - describe('acceptIntent()', () => { + beforeEach(async () => { + dialog.mockRestore(); + handlerInput.requestEnvelope.request.type = 'LaunchRequest'; + handlerInput.requestEnvelope.request.intent.name = 'EstimatedRestoration' + handlerInput.responseBuilder.speak.mockClear() + handlerInput.responseBuilder.reprompt.mockClear() + handlerInput.responseBuilder.addElicitSlotDirective.mockClear() + handlerInput.responseBuilder.addConfirmSlotDirective.mockClear() + handlerInput.responseBuilder.withShouldEndSession.mockClear() + handlerInput.responseBuilder.getResponse.mockClear() + await StateHandler.handle(handlerInput) + handlerInput.requestEnvelope.request.type = 'IntentRequest'; + mockGetSession.mockClear() + mockSaveSession.mockClear() + }); + + describe('routing logic', () => { it('Sends machine to askAboutAddress when new', async () => { - const params = { - conversationStack: [], - currentSubConversation: { estimatedRestoration:{}}, - sessionAttributes: {}, - intent: { - name: 'EstimatedRestoration', - slots: {}, - }, - topConversation: true, - newConversation: false, - poppedConversation: false, - fallThrough: false, - }; + await StateHandler.handle(handlerInput) - const { - conversationStack, - currentSubConversation, - sessionAttributes, - fallThrough, - pop, - } = await estimatedRestoration.acceptIntent(params); - - expect(conversationStack).toEqual([]); - expect(currentSubConversation).toEqual({ - estimatedRestoration:{ - machineState: 'askAboutHomeOrOther', - machineContext: { - ...defaultEstimatedRestorationMachineContext(), - conversationAttributes: {}, - }, - }, - }); - expect(sessionAttributes).toEqual({ conversationAttributes: {}}); - expect(fallThrough).toBeFalsy(); - expect(pop).toBeFalsy(); + expect(getMockState().machineState).toEqual('askAboutHomeOrOther') + expect(getMockState().machineContext.address).toEqual('123 Company Dr') }) - it('Sends machine from askAboutHomeOrOther to selectOtherOutage when user triggers OtherIntent', async () => { - const params = { - conversationStack: [{ - engagement: { - machineState: 'estimatedRestoration', - machineContext: {} - } - }], - currentSubConversation: { - estimatedRestoration: { - machineState: 'askAboutHomeOrOther', - machineContext: { - ...defaultEstimatedRestorationMachineContext(), - conversationAttributes: {}, - }, - }, - }, - sessionAttributes: {}, - intent: { - name: 'OtherIntent', - slots: {}, - }, - topConversation: true, - newConversation: false, - poppedConversation: false, - fallThrough: false, - }; + it('Sends machine from askAboutHomeOrOther to pickFromListQuestion when user triggers OtherIntent', async () => { + handlerInput.requestEnvelope.request.intent.name = 'OtherIntent' + await StateHandler.handle(handlerInput) - const { - conversationStack, - currentSubConversation, - sessionAttributes, - fallThrough, - pop, - } = await estimatedRestoration.acceptIntent(params); - - expect(conversationStack).toEqual(params.conversationStack); - expect(currentSubConversation).toEqual({ - pickAnOutage:{} - }); - expect(sessionAttributes).toEqual({ conversationAttributes: {}}); - expect(fallThrough).toBeFalsy(); - expect(pop).toBeFalsy(); + expect(getMockState().machineState).toEqual('pickFromListQuestion') }) }); - describe('craftResponse()', () => { - beforeEach(() => { - dialog.mockRestore(); - mockGetSession.mockClear() - mockSaveSession.mockClear() - }); - - it('when address is incorrect, tells the user they must correct their address online or by phone to get a restoration estimate (badAddress)', () => { + describe('actual response', () => { + it('when address is incorrect, tells the user they must correct their address online or by phone to get a restoration estimate (badAddress)', async () => { const params = { dialog, subConversation: { @@ -126,13 +72,13 @@ describe('Estimated Restoration Conversation Tests', () => { } }; - estimatedRestoration.craftResponse(params); + await StateHandler.handle(params); expect(dialog.mock.calls[0][0]).toEqual('estimatedRestoration.address.wrongAddress'); expect(dialog.mock.calls[0][1]).toEqual({ website: fakeWebsite, phoneNumber: fakePhoneNumber }); }); - it('when address is correct, gives the user their estimated restoration time (reportEstimateToUser)', () => { + it('when address is correct, gives the user their estimated restoration time (reportEstimateToUser)', async () => { const params = { dialog, subConversation: { @@ -146,13 +92,13 @@ describe('Estimated Restoration Conversation Tests', () => { } }; - estimatedRestoration.craftResponse(params); + await StateHandler.handle(params); expect(dialog.mock.calls[0][0]).toEqual('estimatedRestoration.reply.other'); expect(dialog.mock.calls[0][1]).toEqual({ estimate: '6 hours' }); }); - it('gives the generic error response on errors (error)', () => { + it('gives the generic error response on errors (error)', async () => { const params = { dialog, subConversation: { @@ -166,26 +112,13 @@ describe('Estimated Restoration Conversation Tests', () => { } }; - estimatedRestoration.craftResponse(params); + await StateHandler.handle(params); expect(dialog.mock.calls[0][0]).toEqual('home.error'); }); }); describe('systemic test', () => { - beforeEach(() => { - dialog.mockRestore(); - handlerInput.requestEnvelope.request.type = 'IntentRequest'; - handlerInput.responseBuilder.speak.mockClear() - handlerInput.responseBuilder.reprompt.mockClear() - handlerInput.responseBuilder.addElicitSlotDirective.mockClear() - handlerInput.responseBuilder.addConfirmSlotDirective.mockClear() - handlerInput.responseBuilder.withShouldEndSession.mockClear() - handlerInput.responseBuilder.getResponse.mockClear() - mockGetSession.mockClear() - mockSaveSession.mockClear() - }); - it('sends the machine to correctAddress after confirming address when user is asking about their home', async () => { const requestAttributes = { 'estimatedRestoration.reply.other': 'estimatedRestoration.reply.other' diff --git a/lambda/test/util/defaults.js b/lambda/test/util/defaults.js new file mode 100644 index 0000000..573bbf2 --- /dev/null +++ b/lambda/test/util/defaults.js @@ -0,0 +1,82 @@ +const defaultConversationAttributes = (confirmedAddress = false, correctAddress = false, wipeConversation = false) => ({ + confirmAddress: { + confirmedAddress, + correctAddress, + }, + resume: { + wipeConversation, + }, +}) + +const defaultSessionAttributes = () => ({ + conversationAttributes: defaultConversationAttributes(), +}) + +const emptyIntent = (name) => ({ + name: name, + slots: {} +}) + +const defaultParams = () => ({ + conversationStack: [], + currentSubConversation: {billing:{}}, + sessionAttributes: defaultSessionAttributes(), + intent: {}, + topConversation: true, + newConversation: false, + poppedConversation: false, + fallThrough: false, +}) + +const defaultYesNoIntent = (valueId = 'yes') => ({ + name:'YesNoIntent', + slots: { + yesNo: { + resolutions: { + resolutionsPerAuthority:[ + { + values:[ + { + value:{ + id: valueId, + }, + }, + ], + }, + ], + }, + }, + }, +}) + +const defaultPickALetterIntent = (valueId = 'a') => ({ + name:'LetterIntent', + slots: { + letter: { + resolutions: { + resolutionsPerAuthority:[ + { + values:[ + { + value:{ + id: valueId, + }, + }, + ], + }, + ], + }, + }, + }, +}) + + + +module.exports = { + defaultConversationAttributes, + defaultSessionAttributes, + defaultParams, + defaultYesNoIntent, + defaultPickALetterIntent, + emptyIntent +} diff --git a/lambda/test/util/mockConversationSet.js b/lambda/test/util/mockConversationSet.js deleted file mode 100644 index 08b1097..0000000 --- a/lambda/test/util/mockConversationSet.js +++ /dev/null @@ -1,125 +0,0 @@ -const conversationSet = { - engagement: { - craftResponse: ({ finalWords }) => { - if ( finalWords ) { - return ''; - } - return "engagement text"; - }, - acceptIntent: async ({ conversationStack, currentSubConversation, sessionAttributes, intent, topConversation, poppedConversation, fallThrough }) => { - if (!fallThrough) { - if (topConversation) { - if (!poppedConversation) { - switch (intent.name) { - case 'step1Intent' : { - conversationStack.push({engagement:{}}); - currentSubConversation = {step1: {}}; - return {conversationStack, currentSubConversation, sessionAttributes, fallThrough: false, pop: false}; - } - case 'step2Intent' : { - conversationStack.push({engagement: {}}); - currentSubConversation = {step2: {}}; - return {conversationStack, currentSubConversation, sessionAttributes, fallThrough: false, pop: false}; - } - } - } - } else { - switch (intent.name) { - case 'step2Intent' : { - if (Object.keys(currentSubConversation)[0] != Object.keys({step2: {}})[0] || currentSubConversation.step2.numValue != intent.slots.num.value) { - conversationStack.push(currentSubConversation); - currentSubConversation = {step2: {}}; - return {conversationStack, currentSubConversation, sessionAttributes, fallThrough: false, pop: false}; - } - } - } - } - } - //all accept intents must return these if nothing is changed - return {conversationStack, currentSubConversation, sessionAttributes, fallThrough: false, pop: false}; - }, - }, - step1: { - craftResponse: ({ dialog, conversationStack, subConversation, sessionAttributes, intent, finalWords }) => { - if (subConversation.step1.complain) { - return "you're already in step1"; - } - return "step1 text"; - }, - acceptIntent: async ({ conversationStack, currentSubConversation, sessionAttributes, intent, topConversation, newConversation, poppedConversation, fallThrough }) => { - if (!fallThrough) { - if (topConversation) { - switch (intent.name) { - case 'step1Intent' : { - if (!newConversation) { - currentSubConversation.step1.complain = true; - return {conversationStack, currentSubConversation, sessionAttributes, fallThrough: false, pop: false}; - } - } - } - } - } - return {conversationStack, currentSubConversation, sessionAttributes, fallThrough: false, pop: false}; - }, - }, - step2: { - craftResponse: ({ dialog, conversationStack, subConversation, sessionAttributes, intent, finalWords }) => { - if (subConversation.step2.resumeStatement) { - return "step2 resume"; - } - if (subConversation.step2.numValue && !subConversation.step2.goodValue) { - //confirm numval - return `step2 text ${subConversation.step2.numValue}`; - } else if (subConversation.step2.numValue && intent.slots.good.value === 'yes') { - return "step2 thanks"; - } else { - //ask for numval - return "step2 text"; - } - }, - acceptIntent: async ({ conversationStack, currentSubConversation, sessionAttributes, intent, topConversation, newConversation, poppedConversation, fallThrough }) => { - if (!fallThrough) { - if (topConversation) { - delete currentSubConversation.step2.resumeStatement; - switch(intent.name) { - case 'step2Intent': - if (poppedConversation) { - currentSubConversation.step2.resumeStatement = true; - return {conversationStack, currentSubConversation, sessionAttributes, fallThrough: false, pop: false}; - } - if(intent.slots.num?.value){ - currentSubConversation.step2.numValue = intent.slots.num.value; - } - if(intent.slots.good?.value){ - if (intent.slots.good.value === 'yes') { - //Call an API, pop this conversation while saying something...? - currentSubConversation.step2.goodValue = intent.slots.good.value; - - return {conversationStack, currentSubConversation, sessionAttributes, fallThrough: false, pop: true}; - } else { - delete currentSubConversation.step2.numValue; - delete currentSubConversation.step2.goodValue; - } - } - return {conversationStack, currentSubConversation, sessionAttributes, fallThrough: false, pop: false}; - case 'yesNoIntent': - if (currentSubConversation.step2.numValue) { - if (intent.slots.good.value === 'yes') { - //Call an API, pop this conversation while saying something...? - currentSubConversation.step2.goodValue = intent.slots.yesNo.value; - return {conversationStack, currentSubConversation, sessionAttributes, fallThrough: false, pop: true}; - } else { - delete currentSubConversation.step2.numValue; - delete currentSubConversation.step2.goodValue; - } - } - return {conversationStack, currentSubConversation, sessionAttributes, fallThrough: false, pop: false}; - } - } - } - return {conversationStack, currentSubConversation, sessionAttributes, fallThrough: false, pop: false}; - }, - }, -} - -module.exports = { conversationSet }; diff --git a/lambda/test/util/mockSessionAttributesService.js b/lambda/test/util/mockSessionAttributesService.js deleted file mode 100644 index 7dc4a83..0000000 --- a/lambda/test/util/mockSessionAttributesService.js +++ /dev/null @@ -1,15 +0,0 @@ -const { handlerInput } = require('./mockHandlerInput') - -const mockGetSession = jest.fn(() => handlerInput.attributesManager.getSessionAttributes()) -const mockSaveSession = jest.fn(sessionAttributes => handlerInput.attributesManager.setSessionAttributes(sessionAttributes)) - -jest.mock('../../service/SessionAttributesService', () => ({ - __esModule: true, - getSessionFromDynamoDb: mockGetSession, - saveSessionToDynamoDb: mockSaveSession, -})) - -module.exports = { - mockGetSession, - mockSaveSession, -} diff --git a/lambda/test/util/mocks.js b/lambda/test/util/mocks.js new file mode 100644 index 0000000..4726a40 --- /dev/null +++ b/lambda/test/util/mocks.js @@ -0,0 +1,79 @@ +const mock_ = require('lodash') +const { initialize } = require('@ocelot-consulting/ocelot-voice-framework') +const conversationSet = require('../../conversation') +const dialog = require('../../dialog') + +let mockRequestAttributesKeys = {} +let mockSessionAttributes = {} +let mockSlots = {} + +const setRequestAttributes = newKeys => { + mockRequestAttributesKeys = mock_.merge(mockRequestAttributesKeys, newKeys) +} + +const setSlots = newSlots => { + mockSlots = mock_.merge(mockSlots, newSlots) +} + +const handlerInput = { + attributesManager: { + getRequestAttributes: () => ({ + t: key => { + if (!mockRequestAttributesKeys[key]) console.log(`getRequestAttributes called with missing key: ${key}`); + return mockRequestAttributesKeys[key]; + } + }), + setRequestAttributes, + getSessionAttributes: () => mockSessionAttributes, + setSessionAttributes: newSession => { + mockSessionAttributes = newSession + }, + }, + responseBuilder: { + speak: jest.fn().mockReturnThis(), + withShouldEndSession: jest.fn().mockReturnThis(), + getResponse: jest.fn().mockReturnThis(), + addElicitSlotDirective: jest.fn().mockReturnThis(), + addConfirmSlotDirective: jest.fn().mockReturnThis(), + reprompt: jest.fn().mockReturnThis(), + addDelegateDirective: jest.fn().mockReturnThis(), + withSimpleCard: jest.fn().mockReturnThis(), + }, + requestEnvelope: { + request: { + intent: { + name: 'GenericIntentName', + slots: mockSlots, + }, + type: 'GenericRequestType', + }, + session: { + user: { + userId: 'fakeUserId' + }, + }, + }, +} + +const mockGetSession = jest.fn(() => handlerInput.attributesManager.getSessionAttributes()) +const mockSaveSession = jest.fn(sessionAttributes => handlerInput.attributesManager.setSessionAttributes(sessionAttributes)) + +const getMockState = () => mockGetSession().state.currentSubConversation[Object.keys(mockGetSession().state.currentSubConversation)[0]] + +const { StateHandler } = initialize({ + conversationSet, + dialog, + fetchSession: mockGetSession, + saveSession: mockSaveSession, +}) + +module.exports = { + StateHandler, + mockGetSession, + mockSaveSession, + getMockState, + handlerInput, + getSlots, + setSlots, + dialog, +} From 9c79a821d06d67d6d3fdb80ef9e7b5fcfe8707a5 Mon Sep 17 00:00:00 2001 From: Lucas Date: Tue, 19 Apr 2022 16:41:38 -0500 Subject: [PATCH 03/10] more refactor --- .../conversation/estimatedRestoration.test.js | 39 ++++++++++--------- lambda/test/util/mocks.js | 4 +- 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/lambda/test/conversation/estimatedRestoration.test.js b/lambda/test/conversation/estimatedRestoration.test.js index 0b229e8..ef936f2 100644 --- a/lambda/test/conversation/estimatedRestoration.test.js +++ b/lambda/test/conversation/estimatedRestoration.test.js @@ -1,12 +1,11 @@ const { defaultSessionAttributes, defaultConversationAttributes, defaultYesNoIntent, defaultPickALetterIntent } = require('../util/defaults'); const { - StateHandler, + run, mockGetSession, mockSaveSession, getMockState, handlerInput, setSlots, - dialog, } = require('../util/mocks') const fakePhoneNumber = '111-111-1111' @@ -24,6 +23,8 @@ const defaultEstimatedRestorationMachineContext = () => ({ address: "11477 Olde Cabin Rd Suite 320" }) +const dialog = jest.fn() + describe('Estimated Restoration Conversation Tests', () => { beforeEach(async () => { dialog.mockRestore(); @@ -35,7 +36,7 @@ describe('Estimated Restoration Conversation Tests', () => { handlerInput.responseBuilder.addConfirmSlotDirective.mockClear() handlerInput.responseBuilder.withShouldEndSession.mockClear() handlerInput.responseBuilder.getResponse.mockClear() - await StateHandler.handle(handlerInput) + await run(handlerInput) handlerInput.requestEnvelope.request.type = 'IntentRequest'; mockGetSession.mockClear() mockSaveSession.mockClear() @@ -43,7 +44,7 @@ describe('Estimated Restoration Conversation Tests', () => { describe('routing logic', () => { it('Sends machine to askAboutAddress when new', async () => { - await StateHandler.handle(handlerInput) + await run(handlerInput) expect(getMockState().machineState).toEqual('askAboutHomeOrOther') expect(getMockState().machineContext.address).toEqual('123 Company Dr') @@ -51,7 +52,7 @@ describe('Estimated Restoration Conversation Tests', () => { it('Sends machine from askAboutHomeOrOther to pickFromListQuestion when user triggers OtherIntent', async () => { handlerInput.requestEnvelope.request.intent.name = 'OtherIntent' - await StateHandler.handle(handlerInput) + await run(handlerInput) expect(getMockState().machineState).toEqual('pickFromListQuestion') }) @@ -72,7 +73,7 @@ describe('Estimated Restoration Conversation Tests', () => { } }; - await StateHandler.handle(params); + await run(handlerInput); expect(dialog.mock.calls[0][0]).toEqual('estimatedRestoration.address.wrongAddress'); expect(dialog.mock.calls[0][1]).toEqual({ website: fakeWebsite, phoneNumber: fakePhoneNumber }); @@ -92,7 +93,7 @@ describe('Estimated Restoration Conversation Tests', () => { } }; - await StateHandler.handle(params); + await run(params); expect(dialog.mock.calls[0][0]).toEqual('estimatedRestoration.reply.other'); expect(dialog.mock.calls[0][1]).toEqual({ estimate: '6 hours' }); @@ -112,7 +113,7 @@ describe('Estimated Restoration Conversation Tests', () => { } }; - await StateHandler.handle(params); + await run(params); expect(dialog.mock.calls[0][0]).toEqual('home.error'); }); @@ -128,7 +129,7 @@ describe('Estimated Restoration Conversation Tests', () => { handlerInput.attributesManager.setRequestAttributes(requestAttributes) handlerInput.attributesManager.setSessionAttributes({ ...defaultSessionAttributes, - conversationAttributes:defaultConversationAttributes(true, true), + conversationAttributes: defaultConversationAttributes(true, true), state: { currentSubConversation: { confirmAddress: { @@ -159,7 +160,7 @@ describe('Estimated Restoration Conversation Tests', () => { handlerInput.requestEnvelope.request.intent.name = 'HomeIntent' - await StateHandler.handle(handlerInput) + await run(handlerInput) const { state } = handlerInput.attributesManager.getSessionAttributes() @@ -230,7 +231,7 @@ describe('Estimated Restoration Conversation Tests', () => { handlerInput.requestEnvelope.request.intent.name = 'HomeIntent' - await StateHandler.handle(handlerInput) + await run(handlerInput) const { state } = handlerInput.attributesManager.getSessionAttributes() @@ -277,14 +278,14 @@ describe('Estimated Restoration Conversation Tests', () => { setSlots(slots) handlerInput.requestEnvelope.request.intent.name = 'EstimatedRestoration' - await StateHandler.handle(handlerInput) + await run(handlerInput) expect(handlerInput.responseBuilder.speak.mock.calls.length).toEqual(1) expect(handlerInput.responseBuilder.speak.mock.calls[0][0]).toEqual('estimatedRestoration.homeOrOther.confirm'); handlerInput.responseBuilder.speak.mockClear(); handlerInput.requestEnvelope.request.intent.name = 'HomeIntent' - await StateHandler.handle(handlerInput) + await run(handlerInput) expect(handlerInput.responseBuilder.speak.mock.calls.length).toEqual(1) expect(handlerInput.responseBuilder.speak.mock.calls[0][0]).toEqual('confirmAddress.confirm'); @@ -292,7 +293,7 @@ describe('Estimated Restoration Conversation Tests', () => { handlerInput.requestEnvelope.request.intent.name = 'YesNoIntent' setSlots(defaultYesNoIntent('yes').slots); - await StateHandler.handle(handlerInput) + await run(handlerInput) expect(handlerInput.responseBuilder.speak.mock.calls.length).toEqual(1) expect(handlerInput.responseBuilder.speak.mock.calls[0][0]).toEqual('estimatedRestoration.reply.home home.reEngage'); @@ -300,28 +301,28 @@ describe('Estimated Restoration Conversation Tests', () => { handlerInput.requestEnvelope.request.intent.name = 'EstimatedRestoration' setSlots(slots) - await StateHandler.handle(handlerInput) + await run(handlerInput) expect(handlerInput.responseBuilder.speak.mock.calls.length).toEqual(1) expect(handlerInput.responseBuilder.speak.mock.calls[0][0]).toEqual('estimatedRestoration.homeOrOther.confirm'); handlerInput.responseBuilder.speak.mockClear(); handlerInput.requestEnvelope.request.intent.name = 'HomeIntent' - await StateHandler.handle(handlerInput) + await run(handlerInput) expect(handlerInput.responseBuilder.speak.mock.calls.length).toEqual(1) expect(handlerInput.responseBuilder.speak.mock.calls[0][0]).toEqual('estimatedRestoration.reply.home home.reEngage'); handlerInput.responseBuilder.speak.mockClear(); handlerInput.requestEnvelope.request.intent.name = 'EstimatedRestoration' - await StateHandler.handle(handlerInput) + await run(handlerInput) expect(handlerInput.responseBuilder.speak.mock.calls.length).toEqual(1) expect(handlerInput.responseBuilder.speak.mock.calls[0][0]).toEqual('estimatedRestoration.homeOrOther.confirm'); handlerInput.responseBuilder.speak.mockClear(); handlerInput.requestEnvelope.request.intent.name = 'OtherIntent' - await StateHandler.handle(handlerInput) + await run(handlerInput) expect(handlerInput.responseBuilder.speak.mock.calls.length).toEqual(1) expect(handlerInput.responseBuilder.speak.mock.calls[0][0]).toEqual('estimatedRestoration.otherLocation.confirm'); @@ -329,7 +330,7 @@ describe('Estimated Restoration Conversation Tests', () => { handlerInput.requestEnvelope.request.intent.name = 'PickALetterIntent' setSlots(defaultPickALetterIntent().slots) - await StateHandler.handle(handlerInput) + await run(handlerInput) expect(handlerInput.responseBuilder.speak.mock.calls.length).toEqual(1) expect(handlerInput.responseBuilder.speak.mock.calls[0][0]).toEqual('estimatedRestoration.reply.other home.reEngage'); diff --git a/lambda/test/util/mocks.js b/lambda/test/util/mocks.js index 4726a40..1590ee4 100644 --- a/lambda/test/util/mocks.js +++ b/lambda/test/util/mocks.js @@ -68,12 +68,10 @@ const { StateHandler } = initialize({ }) module.exports = { - StateHandler, + run: async input => StateHandler.handle(input), mockGetSession, mockSaveSession, getMockState, handlerInput, - getSlots, setSlots, - dialog, } From 59ee5de40bdda36c0f5095f3c20a26ae5ca9f7fb Mon Sep 17 00:00:00 2001 From: Lucas Date: Fri, 22 Apr 2022 16:04:32 -0500 Subject: [PATCH 04/10] so close --- lambda/conversation/estimatedRestoration.js | 4 +- .../conversation/estimatedRestoration.test.js | 200 ++++++++++-------- lambda/test/util/mocks.js | 6 +- 3 files changed, 117 insertions(+), 93 deletions(-) diff --git a/lambda/conversation/estimatedRestoration.js b/lambda/conversation/estimatedRestoration.js index 8b44fdc..1fa1326 100644 --- a/lambda/conversation/estimatedRestoration.js +++ b/lambda/conversation/estimatedRestoration.js @@ -84,10 +84,10 @@ const estimatedRestoration = { stateMap, transitionStates: [ 'confirmAddress', 'pickAnOutage' ], dialogMap: { - askAboutHomeOrOther: ({ misunderstandingCount }) => dialog( + askAboutHomeOrOther: ({ misunderstandingCount = 0 }) => dialog( `estimatedRestoration.homeOrOther.${misunderstandingCount > 0 ? 'misheard' : 'confirm'}` ), - selectOtherOutage: ({ misunderstandingCount }) => dialog( + selectOtherOutage: ({ misunderstandingCount = 0 }) => dialog( `estimatedRestoration.otherLocation.${misunderstandingCount > 0 ? 'misheard' : 'confirm'}`, { options: utils.objectMapToSpeech(outageOptions) } ), diff --git a/lambda/test/conversation/estimatedRestoration.test.js b/lambda/test/conversation/estimatedRestoration.test.js index ef936f2..402aba1 100644 --- a/lambda/test/conversation/estimatedRestoration.test.js +++ b/lambda/test/conversation/estimatedRestoration.test.js @@ -4,6 +4,7 @@ const { mockGetSession, mockSaveSession, getMockState, + getResponse, handlerInput, setSlots, } = require('../util/mocks') @@ -23,12 +24,10 @@ const defaultEstimatedRestorationMachineContext = () => ({ address: "11477 Olde Cabin Rd Suite 320" }) -const dialog = jest.fn() - describe('Estimated Restoration Conversation Tests', () => { beforeEach(async () => { - dialog.mockRestore(); handlerInput.requestEnvelope.request.type = 'LaunchRequest'; + await run(handlerInput) handlerInput.requestEnvelope.request.intent.name = 'EstimatedRestoration' handlerInput.responseBuilder.speak.mockClear() handlerInput.responseBuilder.reprompt.mockClear() @@ -36,10 +35,28 @@ describe('Estimated Restoration Conversation Tests', () => { handlerInput.responseBuilder.addConfirmSlotDirective.mockClear() handlerInput.responseBuilder.withShouldEndSession.mockClear() handlerInput.responseBuilder.getResponse.mockClear() - await run(handlerInput) handlerInput.requestEnvelope.request.type = 'IntentRequest'; mockGetSession.mockClear() mockSaveSession.mockClear() + handlerInput.attributesManager.setRequestAttributes({ + 'estimatedRestoration.address.wrongAddress': 'estimatedRestoration.address.wrongAddress', + 'estimatedRestoration.homeOrOther.confirm': 'estimatedRestoration.homeOrOther.confirm', + 'estimatedRestoration.homeOrOther.misheard': 'estimatedRestoration.homeOrOther.misheard', + 'estimatedRestoration.otherLocation.confirm': 'estimatedRestoration.otherLocation.confirm', + 'estimatedRestoration.reply.other': 'estimatedRestoration.reply.other', + 'home.error': 'home.error', + 'confirmAddress.confirm': 'confirmAddress.confirm', + 'confirmAddress.wrongAddress': 'confirmAddress.wrongAddress', + 'estimatedRestoration.reply.home': 'estimatedRestoration.reply.home', + 'home.reEngage': 'home.reEngage', + 'estimatedRestoration.goBack': 'estimatedRestoration.goBack', + 'resume.fresh': 'resume.fresh', + 'home.welcome': 'home.welcome', + 'home.promptResume': 'home.promptResume', + 'home.misheardResume': 'home.misheardResume', + 'estimatedRestoration.homeOrOther.confirm': 'estimatedRestoration.homeOrOther.confirm', + 'estimatedRestoration.resume': 'estimatedRestoration.resume', + }) }); describe('routing logic', () => { @@ -48,6 +65,7 @@ describe('Estimated Restoration Conversation Tests', () => { expect(getMockState().machineState).toEqual('askAboutHomeOrOther') expect(getMockState().machineContext.address).toEqual('123 Company Dr') + expect(getResponse()[0][0]).toEqual('estimatedRestoration.homeOrOther.confirm') }) it('Sends machine from askAboutHomeOrOther to pickFromListQuestion when user triggers OtherIntent', async () => { @@ -55,78 +73,105 @@ describe('Estimated Restoration Conversation Tests', () => { await run(handlerInput) expect(getMockState().machineState).toEqual('pickFromListQuestion') + expect(getResponse()[0][0]).toEqual('estimatedRestoration.otherLocation.confirm') + await run(handlerInput) + + expect(getMockState().machineState).toEqual('pickFromListQuestion') + expect(getResponse()[0][0]).toEqual('estimatedRestoration.otherLocation.confirm') }) - }); - describe('actual response', () => { - it('when address is incorrect, tells the user they must correct their address online or by phone to get a restoration estimate (badAddress)', async () => { - const params = { - dialog, - subConversation: { - estimatedRestoration: { - machineState: 'incorrectAddress', - machineContext: {}, + it('when address is incorrect, tells the user they must correct their address online or by phone to get a restoration estimate (incorrectAddress)', async () => { + handlerInput.attributesManager.setSessionAttributes({ + ...handlerInput.attributesManager.getSessionAttributes(), + state: { + ...handlerInput.attributesManager.getSessionAttributes().state, + currentSubConversation: { + estimatedRestoration: { + machineState: 'addressResult', + machineContext: { + conversationAttributes: { + confirmAddress: { + correctAddress: false, + } + } + }, + }, }, + conversationStack: [] }, - sessionAttributes: { - previousPoppedConversation:'' - } - }; + conversationAttributes: { + confirmAddress: { + correctAddress: false, + }, + }, + }) + handlerInput.requestEnvelope.request.intent.name = 'EstimatedRestoration' await run(handlerInput); - expect(dialog.mock.calls[0][0]).toEqual('estimatedRestoration.address.wrongAddress'); - expect(dialog.mock.calls[0][1]).toEqual({ website: fakeWebsite, phoneNumber: fakePhoneNumber }); + expect(getMockState().machineState).toEqual('incorrectAddress') + expect(getResponse()[0][0]).toEqual('estimatedRestoration.address.wrongAddress') }); it('when address is correct, gives the user their estimated restoration time (reportEstimateToUser)', async () => { - const params = { - dialog, - subConversation: { - estimatedRestoration: { - machineState: 'reportEstimateToUser', - machineContext: { estimate: '6 hours' }, + handlerInput.attributesManager.setSessionAttributes({ + ...handlerInput.attributesManager.getSessionAttributes(), + state: { + ...handlerInput.attributesManager.getSessionAttributes().state, + currentSubConversation: { + estimatedRestoration: { + machineState: 'reportEstimateToUser', + machineContext: { + conversationAttributes: { + confirmAddress: { + correctAddress: true, + } + }, + estimate: '6 hours', + }, + }, }, + conversationStack: [], }, - sessionAttributes: { - previousPoppedConversation:'' + conversationAttributes: { + conversationAttributes: { + confirmAddress: { + correctAddress: true, + } + } } - }; + }) - await run(params); + await run(handlerInput); - expect(dialog.mock.calls[0][0]).toEqual('estimatedRestoration.reply.other'); - expect(dialog.mock.calls[0][1]).toEqual({ estimate: '6 hours' }); + expect(getMockState().machineState).toEqual('reportEstimateToUser') + expect(getResponse()[0][0]).toEqual('estimatedRestoration.reply.other'); }); it('gives the generic error response on errors (error)', async () => { - const params = { - dialog, - subConversation: { - estimatedRestoration: { - machineState: 'error', - machineContext: { error: 'fake error' }, + handlerInput.attributesManager.setSessionAttributes({ + ...handlerInput.attributesManager.getSessionAttributes(), + state: { + ...handlerInput.attributesManager.getSessionAttributes().state, + currentSubConversation: { + estimatedRestoration: { + machineState: 'error', + machineContext: { error: 'fake error' }, + }, }, - }, - sessionAttributes: { - previousPoppedConversation:'' + conversationStack: [], } - }; + }) - await run(params); + await run(handlerInput); - expect(dialog.mock.calls[0][0]).toEqual('home.error'); + expect(getMockState().machineState).toEqual('error') + expect(getResponse()[0][0]).toEqual('home.error'); }); }); describe('systemic test', () => { it('sends the machine to correctAddress after confirming address when user is asking about their home', async () => { - const requestAttributes = { - 'estimatedRestoration.reply.other': 'estimatedRestoration.reply.other' - } - handlerInput.attributesManager.setRequestAttributes(requestAttributes) - - handlerInput.attributesManager.setRequestAttributes(requestAttributes) handlerInput.attributesManager.setSessionAttributes({ ...defaultSessionAttributes, conversationAttributes: defaultConversationAttributes(true, true), @@ -155,9 +200,7 @@ describe('Estimated Restoration Conversation Tests', () => { } }) - const slots1 = {} - setSlots(slots1) - + setSlots({}) handlerInput.requestEnvelope.request.intent.name = 'HomeIntent' await run(handlerInput) @@ -184,19 +227,13 @@ describe('Estimated Restoration Conversation Tests', () => { }, }, }) - expect(handlerInput.responseBuilder.speak.mock.calls.length).toEqual(1) + expect(getResponse().length).toEqual(1) }); it('sends the machine to incorrectAddress after confirming address when user is asking about their home', async () => { - const requestAttributes = { - 'estimatedRestoration.address.wrongAddress': 'estimatedRestoration.address.wrongAddress' - } - handlerInput.attributesManager.setRequestAttributes(requestAttributes) - - handlerInput.attributesManager.setRequestAttributes(requestAttributes) handlerInput.attributesManager.setSessionAttributes({ ...defaultSessionAttributes, - conversationAttributes:defaultConversationAttributes(true, false), + conversationAttributes: defaultConversationAttributes(true, false), state: { currentSubConversation: { confirmAddress: { @@ -226,9 +263,7 @@ describe('Estimated Restoration Conversation Tests', () => { } }) - const slots1 = {} - setSlots(slots1) - + setSlots({}) handlerInput.requestEnvelope.request.intent.name = 'HomeIntent' await run(handlerInput) @@ -246,40 +281,27 @@ describe('Estimated Restoration Conversation Tests', () => { } } }) - expect(handlerInput.responseBuilder.speak.mock.calls.length).toEqual(1) + expect(getResponse().length).toEqual(1) }); it('walks through the expected path of estimatedRestoration', async () => { - const requestAttributes = { - 'estimatedRestoration.reply.other': 'estimatedRestoration.reply.other', - 'estimatedRestoration.address.wrongAddress': 'estimatedRestoration.address.wrongAddress', - 'estimatedRestoration.homeOrOther.confirm': 'estimatedRestoration.homeOrOther.confirm', - 'confirmAddress.confirm': 'confirmAddress.confirm', - 'estimatedRestoration.reply.home': 'estimatedRestoration.reply.home', - 'home.reEngage': 'home.reEngage', - 'estimatedRestoration.otherLocation.confirm': 'estimatedRestoration.otherLocation.confirm' - } - handlerInput.attributesManager.setRequestAttributes(requestAttributes) handlerInput.attributesManager.setSessionAttributes({ - ...defaultSessionAttributes, - conversationAttributes:{ - ...defaultConversationAttributes(false, false), - }, + ...handlerInput.attributesManager.getSessionAttributes(), state: { - currentSubConversation: { - engagement: {} - }, - conversationStack: [ - ], - } + ...handlerInput.attributesManager.getSessionAttributes().state, + currentSubConversation: { estimatedRestoration: {}}, + conversationStack: [], + }, + conversationAttributes: {}, }) - const slots = {} - setSlots(slots) - - handlerInput.requestEnvelope.request.intent.name = 'EstimatedRestoration' + console.log('before: ', handlerInput.attributesManager.getSessionAttributes()) + // setSlots({}) await run(handlerInput) + console.log('after: ', handlerInput.attributesManager.getSessionAttributes()) + + expect(handlerInput.responseBuilder.speak.mock.calls.length).toEqual(1) expect(handlerInput.responseBuilder.speak.mock.calls[0][0]).toEqual('estimatedRestoration.homeOrOther.confirm'); handlerInput.responseBuilder.speak.mockClear(); @@ -296,7 +318,7 @@ describe('Estimated Restoration Conversation Tests', () => { await run(handlerInput) expect(handlerInput.responseBuilder.speak.mock.calls.length).toEqual(1) - expect(handlerInput.responseBuilder.speak.mock.calls[0][0]).toEqual('estimatedRestoration.reply.home home.reEngage'); + expect(handlerInput.responseBuilder.speak.mock.calls[0][0]).toEqual('estimatedRestoration.reply.home'); handlerInput.responseBuilder.speak.mockClear(); handlerInput.requestEnvelope.request.intent.name = 'EstimatedRestoration' diff --git a/lambda/test/util/mocks.js b/lambda/test/util/mocks.js index 1590ee4..466e913 100644 --- a/lambda/test/util/mocks.js +++ b/lambda/test/util/mocks.js @@ -30,7 +30,7 @@ const handlerInput = { }, }, responseBuilder: { - speak: jest.fn().mockReturnThis(), + speak: jest.fn(x => console.log('this is x: ', x)).mockReturnThis(), withShouldEndSession: jest.fn().mockReturnThis(), getResponse: jest.fn().mockReturnThis(), addElicitSlotDirective: jest.fn().mockReturnThis(), @@ -59,6 +59,7 @@ const mockGetSession = jest.fn(() => handlerInput.attributesManager.getSessionAt const mockSaveSession = jest.fn(sessionAttributes => handlerInput.attributesManager.setSessionAttributes(sessionAttributes)) const getMockState = () => mockGetSession().state.currentSubConversation[Object.keys(mockGetSession().state.currentSubConversation)[0]] +const getResponse = () => handlerInput.responseBuilder.speak.mock.calls const { StateHandler } = initialize({ conversationSet, @@ -68,10 +69,11 @@ const { StateHandler } = initialize({ }) module.exports = { - run: async input => StateHandler.handle(input), + run: async input => await StateHandler.handle(input), mockGetSession, mockSaveSession, getMockState, + getResponse, handlerInput, setSlots, } From d5e05d999ef6e3d5e825ddfa91e344b8471ce50f Mon Sep 17 00:00:00 2001 From: Lucas Date: Fri, 22 Apr 2022 16:19:00 -0500 Subject: [PATCH 05/10] estimatedRestoration tests done --- .../conversation/estimatedRestoration.test.js | 80 +++++++++++++------ 1 file changed, 54 insertions(+), 26 deletions(-) diff --git a/lambda/test/conversation/estimatedRestoration.test.js b/lambda/test/conversation/estimatedRestoration.test.js index 402aba1..05b0f8e 100644 --- a/lambda/test/conversation/estimatedRestoration.test.js +++ b/lambda/test/conversation/estimatedRestoration.test.js @@ -9,9 +9,6 @@ const { setSlots, } = require('../util/mocks') -const fakePhoneNumber = '111-111-1111' -const fakeWebsite = 'google.com' - const defaultEstimatedRestorationMachineContext = () => ({ selectedHome: false, selectedOutage: '', @@ -322,21 +319,54 @@ describe('Estimated Restoration Conversation Tests', () => { handlerInput.responseBuilder.speak.mockClear(); handlerInput.requestEnvelope.request.intent.name = 'EstimatedRestoration' - setSlots(slots) + + // if the user asks for an estimate after already confirming their address, they get the estimate immediately await run(handlerInput) expect(handlerInput.responseBuilder.speak.mock.calls.length).toEqual(1) - expect(handlerInput.responseBuilder.speak.mock.calls[0][0]).toEqual('estimatedRestoration.homeOrOther.confirm'); + expect(handlerInput.responseBuilder.speak.mock.calls[0][0]).toEqual('estimatedRestoration.reply.home'); handlerInput.responseBuilder.speak.mockClear(); - handlerInput.requestEnvelope.request.intent.name = 'HomeIntent' - await run(handlerInput) + const { state } = handlerInput.attributesManager.getSessionAttributes() - expect(handlerInput.responseBuilder.speak.mock.calls.length).toEqual(1) - expect(handlerInput.responseBuilder.speak.mock.calls[0][0]).toEqual('estimatedRestoration.reply.home home.reEngage'); - handlerInput.responseBuilder.speak.mockClear(); + expect(state.currentSubConversation).toEqual({ + estimatedRestoration: { + machineState: 'reportEstimateToUser', + machineContext: { + selectedHome: true, + selectedOutage: '', + estimate: '3 hours', + misunderstandingCount: 0, + error: '', + previousMachineState: 'reportEstimateToUser', + resuming: false, + conversationAttributes: { + confirmAddress: { + confirmedAddress: true, + correctAddress: true, + }, + }, + address: '123 Company Dr' + } + } + }) + }) + + it('walks through the expected path of estimatedRestoration (second systemic test)', async () => { + handlerInput.attributesManager.setSessionAttributes({ + ...handlerInput.attributesManager.getSessionAttributes(), + state: { + ...handlerInput.attributesManager.getSessionAttributes().state, + currentSubConversation: { estimatedRestoration: { + machineState: 'fresh', + machineContext: {}, + }}, + conversationStack: [], + }, + conversationAttributes: {}, + }) + handlerInput.requestEnvelope.request.intent.name = 'EstimateRestoration' - handlerInput.requestEnvelope.request.intent.name = 'EstimatedRestoration' await run(handlerInput) expect(handlerInput.responseBuilder.speak.mock.calls.length).toEqual(1) @@ -355,28 +385,26 @@ describe('Estimated Restoration Conversation Tests', () => { await run(handlerInput) expect(handlerInput.responseBuilder.speak.mock.calls.length).toEqual(1) - expect(handlerInput.responseBuilder.speak.mock.calls[0][0]).toEqual('estimatedRestoration.reply.other home.reEngage'); + expect(handlerInput.responseBuilder.speak.mock.calls[0][0]).toEqual('estimatedRestoration.reply.other'); handlerInput.responseBuilder.speak.mockClear(); const { state } = handlerInput.attributesManager.getSessionAttributes() expect(state.currentSubConversation).toEqual({ - engagement: { - machineState: 'resume', + estimatedRestoration: { + machineState: 'reportEstimateToUser', machineContext: { + selectedHome: false, + selectedOutage: '', + estimate: '3 hours', + misunderstandingCount: 0, + error: '', + previousMachineState: 'pickAnOutage', resuming: true, - previousMachineState: "estimatedRestoration", - conversationAttributes: { - confirmAddress: { - correctAddress: true, - confirmedAddress: true, - }, - resume: { - wipeConversation: false, - } - }, - }, - }, + conversationAttributes: {}, + address: '123 Company Dr' + } + } }) }); }); From c35baabee4535ec07a28610dcbdfc90c419aaf88 Mon Sep 17 00:00:00 2001 From: Oren Montano Date: Mon, 25 Apr 2022 17:36:57 -0500 Subject: [PATCH 06/10] Adding numbers --- .../interactionModels/custom/en-US.json | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/skill-package/interactionModels/custom/en-US.json b/skill-package/interactionModels/custom/en-US.json index 1997685..2f8a739 100644 --- a/skill-package/interactionModels/custom/en-US.json +++ b/skill-package/interactionModels/custom/en-US.json @@ -145,6 +145,40 @@ "estimated restoration time" ] }, + { + "name": "ANumber", + "slots": [ + { + "name": "number", + "type": "AMAZON.NUMBER", + "samples": [ + "{number}" + ] + } + ], + "samples": [ + "{number}", + "house number {number}", + "the number is {number}" + ] + }, + { + "name": "APhoneNumber", + "slots": [ + { + "name": "phoneNumber", + "type": "AMAZON.PhoneNumber", + "samples": [ + "{phoneNumber}" + ] + } + ], + "samples": [ + "{phoneNumber}", + "my number is {phoneNumber}", + "my phone number is {phoneNumber}" + ] + }, { "name": "ReportOutage", "slots": [ @@ -302,6 +336,34 @@ "confirmationRequired": false, "prompts": {} }, + { + "name": "ANumber", + "confirmationRequired": false, + "prompts": {}, + "slots": [ + { + "name": "number", + "type": "AMAZON.NUMBER", + "elicitationRequired": false, + "confirmationRequired": false, + "prompts": {} + } + ] + }, + { + "name": "APhoneNumber", + "confirmationRequired": false, + "prompts": {}, + "slots": [ + { + "name": "phoneNumber", + "type": "AMAZON.PhoneNumber", + "elicitationRequired": false, + "confirmationRequired": false, + "prompts": {} + } + ] + }, { "name": "OtherIntent", "slots": [], From c7faa0abefc14cd46e0b25d2f2fe11153f215fc2 Mon Sep 17 00:00:00 2001 From: Oren Montano Date: Mon, 25 Apr 2022 21:49:44 -0500 Subject: [PATCH 07/10] New Report Outage --- lambda/conversation/reportOutage.js | 134 +++++++++++++++++++++++----- lambda/dialog/ReportOutageDialog.js | 52 ++++++++++- 2 files changed, 161 insertions(+), 25 deletions(-) diff --git a/lambda/conversation/reportOutage.js b/lambda/conversation/reportOutage.js index a011d8e..0329255 100644 --- a/lambda/conversation/reportOutage.js +++ b/lambda/conversation/reportOutage.js @@ -9,38 +9,79 @@ const { const { utils } = require('@ocelot-consulting/ocelot-voice-framework') const { fakePhoneNumber, fakeWebsite } = require('../constants') -const callOutageApi = async () => { - console.log('outage reported') +const callOutageApi = async ({houseNumber, phoneNumber, attemptCount}) => { + console.log('outage reported', houseNumber, phoneNumber, attemptCount) + + return [{ + result: 'noOutage' + }, { + result: 'yesOutage', + impact: '300', + areaDescription: 'The pines neighborhood north of Park Street', + workDescription: 'Crews are on the scenen and expect repairs to complete in about an hour.' + }, { + result: 'badCombination' + }][attemptCount % 3] } const stateMap = { fresh: state( - transition('processIntent', 'confirmAddress', - guard(({ conversationAttributes }) => !conversationAttributes.confirmAddress?.confirmedAddress), + transition('processIntent', 'askForHouseNumber', + guard(ctx => ctx.houseNumber === 0), ), - transition('processIntent', 'incorrectAddress', - guard(({ conversationAttributes }) => !conversationAttributes.confirmAddress?.correctAddress), + transition('processIntent', 'askForTelephoneNumber', + guard(ctx => ctx.phoneNumber === ''), ), - transition('processIntent', 'letYouKnow', - guard(({ conversationAttributes }) => conversationAttributes.confirmAddress?.correctAddress), + immediate('gotAllData'), + ), + askForHouseNumber: state( + transition('processIntent', 'askForTelephoneNumber', + guard((ctx, { intent }) => intent.name === 'ANumber'), + reduce((ctx, { intent } ) => ({ ...ctx, houseNumber: intent.slots.number.value, misunderstandingCount: 0 })) + ), + transition('processIntent', 'goBack', + guard(({ misunderstandingCount }, { intent }) => intent.name === 'GoBackIntent' || misunderstandingCount > 3) + ), + transition('processIntent', 'askForHouseNumber', + reduce(ctx => ({ ...ctx, misunderstandingCount: ctx.misunderstandingCount + 1 })), ), ), - confirmAddress: state( - immediate('incorrectAddress', - guard(({ resuming, conversationAttributes }) => resuming && !conversationAttributes.confirmAddress?.correctAddress), + askForTelephoneNumber: state( + transition('processIntent', 'gotAllData', + guard((ctx, { intent }) => intent.name === 'APhoneNumber'), + reduce((ctx, { intent } ) => ({ ...ctx, phoneNumber: intent.slots.phoneNumber.value, misunderstandingCount: 0 })) + ), + transition('processIntent', 'goBack', + guard(({ misunderstandingCount }, { intent }) => intent.name === 'GoBackIntent' || misunderstandingCount > 3) ), - immediate('letYouKnow', - guard(({ resuming, conversationAttributes }) => resuming && conversationAttributes.confirmAddress?.correctAddress), + transition('processIntent', 'askForTelephoneNumber', + reduce(ctx => ({ ...ctx, misunderstandingCount: ctx.misunderstandingCount + 1 })), ), ), letYouKnow: state( - transition('processIntent', 'gotAllData', + transition('processIntent', 'thanksForReporting', guard((ctx, { intent }) => intent.name === 'YesNoIntent' && utils.getSlotValueId(intent.slots.yesNo) === 'yes'), - reduce(ctx => ({ ...ctx, letThemKnow: true })) + reduce(ctx => ({ ...ctx, letThemKnow: true, misunderstandingCount: 0 })) ), - transition('processIntent', 'gotAllData', + transition('processIntent', 'thanksForReporting', + guard((ctx, { intent }) => intent.name === 'YesNoIntent' && utils.getSlotValueId(intent.slots.yesNo) === 'no'), + reduce(ctx => ({ ...ctx, letThemKnow: false, misunderstandingCount: 0 })) + ), + transition('processIntent', 'goBack', + guard(({ misunderstandingCount }, { intent }) => intent.name === 'GoBackIntent' || misunderstandingCount > 3) + ), + transition('processIntent', 'letYouKnow', + reduce(ctx => ({ ...ctx, misunderstandingCount: ctx.misunderstandingCount + 1 })), + ), + ), + letYouKnowWOutageReport: state( + transition('processIntent', 'thanksForReporting', + guard((ctx, { intent }) => intent.name === 'YesNoIntent' && utils.getSlotValueId(intent.slots.yesNo) === 'yes'), + reduce(ctx => ({ ...ctx, letThemKnow: true, misunderstandingCount: 0 })) + ), + transition('processIntent', 'thanksForReporting', guard((ctx, { intent }) => intent.name === 'YesNoIntent' && utils.getSlotValueId(intent.slots.yesNo) === 'no'), - reduce(ctx => ({ ...ctx, letThemKnow: false })) + reduce(ctx => ({ ...ctx, letThemKnow: false, misunderstandingCount: 0 })) ), transition('processIntent', 'goBack', guard(({ misunderstandingCount }, { intent }) => intent.name === 'GoBackIntent' || misunderstandingCount > 3) @@ -49,14 +90,54 @@ const stateMap = { reduce(ctx => ({ ...ctx, misunderstandingCount: ctx.misunderstandingCount + 1 })), ), ), + reportAnOutage: state( + transition('processIntent', 'letYouKnow', + guard((ctx, { intent }) => intent.name === 'YesNoIntent' && utils.getSlotValueId(intent.slots.yesNo) === 'yes'), + reduce(ctx => ({ ...ctx, reportingOutage: true, misunderstandingCount: 0 })) + ), + transition('processIntent', 'haveANiceDay', + guard((ctx, { intent }) => intent.name === 'YesNoIntent' && utils.getSlotValueId(intent.slots.yesNo) === 'no'), + reduce(ctx => ({ ...ctx, reportingOutage: false, misunderstandingCount: 0 })) + ), + transition('processIntent', 'goBack', + guard(({ misunderstandingCount }, { intent }) => intent.name === 'GoBackIntent' || misunderstandingCount > 3) + ), + transition('processIntent', 'reportAnOutage', + reduce(ctx => ({ ...ctx, misunderstandingCount: ctx.misunderstandingCount + 1 })), + ), + ), + tryAgain: state( + transition('processIntent', 'askForHouseNumber', + guard((ctx, { intent }) => intent.name === 'YesNoIntent' && utils.getSlotValueId(intent.slots.yesNo) === 'yes'), + reduce(ctx => ({ ...ctx, houseNumber: 0, phoneNumber: '', misunderstandingCount: 0 })) + ), + transition('processIntent', 'goBack', + guard((ctx, { intent }) => intent.name === 'YesNoIntent' && utils.getSlotValueId(intent.slots.yesNo) === 'no'), + reduce(ctx => ({ ...ctx, misunderstandingCount: 0 })) + ), + transition('processIntent', 'goBack', + guard(({ misunderstandingCount }, { intent }) => intent.name === 'GoBackIntent' || misunderstandingCount > 3) + ), + transition('processIntent', 'tryAgain', + reduce(ctx => ({ ...ctx, misunderstandingCount: ctx.misunderstandingCount + 1 })), + ), + ), gotAllData: invoke(callOutageApi, - transition('done', 'thanksForReporting'), + transition('done', 'reportAnOutage', + guard((ctx, {data: { result }}) => result === 'noOutage'), + reduce((ctx, { data }) => ({ ...ctx, attemptCount: ctx.attemptCount + 1 })),), + transition('done', 'letYouKnowWOutageReport', + guard((ctx, {data: { result }}) => result === 'yesOutage'), + reduce((ctx, { data }) => ({ ...ctx, attemptCount: ctx.attemptCount + 1, outageDetails: {...data} })),), + transition('done', 'tryAgain', + guard((ctx, {data: { result }}) => result === 'badCombination'), + reduce((ctx, { data }) => ({ ...ctx, attemptCount: ctx.attemptCount + 1 })),), transition('error', 'error', reduce((ctx, { error }) => ({ ...ctx, error })), ) ), thanksForReporting: state(), - incorrectAddress: state(), + haveANiceDay: state(), error: state(), goBack: state(), }; @@ -64,19 +145,28 @@ const stateMap = { const reportOutage = { handle: ({ dialog, sessionAttributes }) => ({ initialState: { + outageDetails: {}, + attemptCount: 0, + houseNumber: 0, + phoneNumber: '', + reportAnOutage: '', letThemKnow: '', + reportingOutage: '', misunderstandingCount: 0, error: '', }, stateMap, - transitionStates: 'confirmAddress', dialogMap: { + askForHouseNumber: ({ misunderstandingCount }) => dialog(`reportOutage.askForHouseNumber.${misunderstandingCount > 0 ? 'misheard' : 'confirm'}`), + askForTelephoneNumber: ({ misunderstandingCount }) => dialog(`reportOutage.askForTelephoneNumber.${misunderstandingCount > 0 ? 'misheard' : 'confirm'}`), letYouKnow: ({ misunderstandingCount }) => dialog(`reportOutage.letYouKnow.${misunderstandingCount > 0 ? 'misheard' : 'confirm'}`), + letYouKnowWOutageReport: ({ misunderstandingCount, outageDetails }) => dialog(`reportOutage.letYouKnowWOutageReport.${misunderstandingCount > 0 ? 'misheard' : 'confirm'}`, {...outageDetails}), + reportAnOutage: ({ misunderstandingCount }) => dialog(`reportOutage.reportAnOutage.${misunderstandingCount > 0 ? 'misheard' : 'confirm'}`), + tryAgain: ({ misunderstandingCount, houseNumber, phoneNumber }) => dialog(`reportOutage.tryAgain.${misunderstandingCount > 0 ? 'misheard' : 'confirm'}`, {houseNumber, phoneNumber}), thanksForReporting: ({ letThemKnow }) => dialog(`reportOutage.reply.${letThemKnow ? 'withContact' : 'noContact'}`), - incorrectAddress: () => dialog('reportOutage.wrongAddress', { website: fakeWebsite, phoneNumber: fakePhoneNumber }), + haveANiceDay: () => dialog(`reportOutage.reply.haveANiceDay`), error: () => dialog('home.error'), }, - overrideResume: sessionAttributes.previousPoppedConversation === 'confirmAddress', }), intent: 'ReportOutage', canInterrupt: true, diff --git a/lambda/dialog/ReportOutageDialog.js b/lambda/dialog/ReportOutageDialog.js index 6d9746f..c81a999 100644 --- a/lambda/dialog/ReportOutageDialog.js +++ b/lambda/dialog/ReportOutageDialog.js @@ -1,7 +1,40 @@ const reportOutage = { - wrongAddress: [ - `Sorry, we can't report an outage for you because we don't have the right address for your account. Please call {{phoneNumber}} or visit {{website}} to report the outage`, - ], + askForHouseNumber: { + confirm:[ + `What is the house number for the house that you would like to report an outage at?` + ], + misheard: [ + `Please just say a number for the address you're trying to report an outage at.` + ] + }, + askForTelephoneNumber: { + confirm:[ + `What is the telephone number for service at this address?` + ], + misheard: [ + `Please say the telephone number for this address as a ten digit number.` + ] + }, + tryAgain: { + confirm: [ + `I'm sorry but house number {{houseNumber}} and phone number {{phoneNumber}} don't match our records. Would you like to try saying the numbers again?` + ], + misheard: [ + `Was that a yes or a no.`, + `I didn't catch that. You can respond yes or no.`, + `I didn't catch that. Would you like to try saying the numbers again?` + ] + }, + reportAnOutage: { + confirm: [ + `There are no reported outages in your area. Would you like to report an outage at your address?` + ], + misheard: [ + `Was that a yes or a no.`, + `I didn't catch that. You can respond yes or no.`, + `I didn't catch that. Would you like to report an outage at your address?` + ] + }, letYouKnow: { confirm: [ `Would you like a notification when the power is restored?`, @@ -14,12 +47,25 @@ const reportOutage = { `I didn't catch that. Would you like us to notify you when the power is restored?` ] }, + letYouKnowWOutageReport: { + confirm: [ + `There is currently an outage in your area affecting {{impact}} customers in {{areaDescription}}. {{workDescription}} Would you like a notification when the power is restored?` + ], + misheard: [ + `Was that a yes or a no.`, + `I didn't catch that. You can respond yes or no.`, + `I didn't catch that. Would you like us to notify you when the power is restored?` + ] + }, reply: { withContact: [ `Great! We received your outage report, and we'll let you know when power is restored. Thank you.` ], noContact: [ `Great! We received your outage report. Thank you.` + ], + haveANiceDay: [ + `Thank you. Come back if there is a problem.` ] } } From 10929f015e0aa4e75c6b894e7884e0d190f9b061 Mon Sep 17 00:00:00 2001 From: Lucas Date: Tue, 26 Apr 2022 17:21:57 -0500 Subject: [PATCH 08/10] getting there --- lambda/conversation/billing.js | 17 +- lambda/service/fetchBill.js | 15 + lambda/test/conversation/billing.test.js | 479 ++++++------------ .../conversation/estimatedRestoration.test.js | 33 +- lambda/test/conversation/reportOutage.test.js | 223 ++++---- 5 files changed, 295 insertions(+), 472 deletions(-) create mode 100644 lambda/service/fetchBill.js diff --git a/lambda/conversation/billing.js b/lambda/conversation/billing.js index 994e4a6..dfb1c71 100644 --- a/lambda/conversation/billing.js +++ b/lambda/conversation/billing.js @@ -7,20 +7,7 @@ const { guard, } = require('robot3') const { fakePhoneNumber, fakeWebsite } = require('../constants') - -const generateRandomBillAmount = () => { - const min = Math.ceil(0) - const max = Math.floor(500) - const decimalMax = Math.floor(99) - - const dollars = Math.floor(Math.random() * (max - min + 1)) + min - const cents = Math.floor(Math.random() * (decimalMax - min + 1)) + min - - return `$${dollars}.${cents}` -} - -// replace this with an api call -const fetchBillForAddress = async () => ({ billAmount: generateRandomBillAmount() }) +const { fetchBill } = require('../service/fetchBill') const stateMap = { fresh: state( @@ -42,7 +29,7 @@ const stateMap = { guard(({ resuming, conversationAttributes }) => resuming && conversationAttributes.confirmAddress?.correctAddress), ), ), - correctAddress: invoke(fetchBillForAddress, + correctAddress: invoke(fetchBill, transition('done', 'returnBill', reduce((ctx, { data: { billAmount }}) => ({ ...ctx, billAmount })), ), diff --git a/lambda/service/fetchBill.js b/lambda/service/fetchBill.js new file mode 100644 index 0000000..83935e8 --- /dev/null +++ b/lambda/service/fetchBill.js @@ -0,0 +1,15 @@ +const generateRandomBillAmount = () => { + const min = Math.ceil(0) + const max = Math.floor(500) + const decimalMax = Math.floor(99) + + const dollars = Math.floor(Math.random() * (max - min + 1)) + min + const cents = Math.floor(Math.random() * (decimalMax - min + 1)) + min + + return `$${dollars}.${cents}` +} + +// replace this with an api call +const fetchBill = async () => ({ billAmount: generateRandomBillAmount() }) + +module.exports = { fetchBill, generateRandomBillAmount } diff --git a/lambda/test/conversation/billing.test.js b/lambda/test/conversation/billing.test.js index c8b0e4c..425d186 100644 --- a/lambda/test/conversation/billing.test.js +++ b/lambda/test/conversation/billing.test.js @@ -1,377 +1,184 @@ - -const { mockGetSession, mockSaveSession } = require('../util/mockSessionAttributesService') -const { billing } = require('../../conversation/billing') -const { fakePhoneNumber, fakeWebsite } = require('../constants') -const { handlerInput, setSlots } = require('../util/mockHandlerInput') - const { StateHandler } = require('../../conversation'); - +const { + run, + mockGetSession, + mockSaveSession, + getMockState, + getResponse, + handlerInput, + setSlots, +} = require('../util/mocks') const { defaultConversationAttributes, defaultSessionAttributes, - defaultParams, defaultYesNoIntent, - emptyIntent -} = require('./subConversations/defaults') - -const dialog = jest.fn(); - -const defaultBillingMachineContext = () => ({ - conversationAttributes:defaultConversationAttributes(), - billAmount:"", - misunderstandingCount:0, - error:"", - previousMachineState: "fresh", - resuming: false -}) - -const defaultBillingConversation = () => ({ - billing:{ - machineState:"confirmAddress", - machineContext:defaultBillingMachineContext() - } -}) +} = require('../util/defaults') +const fetchBill = require('../../service/fetchBill') describe('Billing Conversation Tests', () => { - describe('acceptIntent()', () => { - it('Sends machine to confirmAddress when new', async () => { - const params = {...defaultParams(), intent: emptyIntent('Billing')}; - - const { - conversationStack, - currentSubConversation, - sessionAttributes, - fallThrough, - pop, - } = await billing.acceptIntent(params); - - expect(conversationStack).toEqual([ - defaultBillingConversation() - ]); - expect(currentSubConversation).toEqual( - { - "confirmAddress":{} - } - ); - expect(sessionAttributes).toEqual(defaultSessionAttributes()); - expect(fallThrough).toBeFalsy(); - expect(pop).toBeFalsy(); - }) - - it('Sends machine to correctAddress on correctAddress:true', async () => { - const params = {...defaultParams(), - currentSubConversation: { - billing: {...defaultBillingConversation().billing, - machineContext: { - ...defaultBillingMachineContext(), - conversationAttributes: defaultConversationAttributes(true, true), - previousMachineState: "confirmAddress" - } - } - }, - sessionAttributes: {...defaultSessionAttributes(), - conversationAttributes: defaultConversationAttributes(true, true) - }, - intent: defaultYesNoIntent(), - poppedConversation:true - }; - - const { - conversationStack, - currentSubConversation, - sessionAttributes, - fallThrough, - pop, - } = await billing.acceptIntent(params); - - expect(conversationStack).toEqual([]); - expect(currentSubConversation).toEqual( - { - billing:{ ...defaultBillingConversation().billing, - machineState:"reportBillToUser", - machineContext: {...defaultBillingMachineContext(), - conversationAttributes: defaultConversationAttributes(true, true), - billAmount:currentSubConversation.billing.machineContext.billAmount, - previousMachineState:"confirmAddress", - resuming: true - } - } - } - ); - expect(sessionAttributes).toEqual({...defaultSessionAttributes(), - conversationAttributes:defaultConversationAttributes(true, true) - }); - expect(fallThrough).toBeFalsy(); - expect(pop).toBeTruthy(); + beforeEach(async () => { + handlerInput.attributesManager.setRequestAttributes({ + 'home.welcome': 'home.welcome', + 'home.engage': 'home.engage', + 'confirmAddress.confirm': 'confirmAddress.confirm', + 'confirmAddress.misheard': 'confirmAddress.misheard', + 'billing.returnBill': 'billing.returnBill', + 'billing.incorrectAddress': 'billing.incorrectAddress', + 'estimatedRestoration.homeOrOther.confirm': 'estimatedRestoration.homeOrOther.confirm', + 'estimatedRestoration.homeOrOther.misheard': 'estimatedRestoration.homeOrOther.misheard', + 'confirmAddress.resume': 'confirmAddress.resume', + 'estimatedRestoration.reply.home': 'estimatedRestoration.reply.home', + 'home.reEngage': 'home.reEngage', + 'home.promptResume': 'home.promptResume', }) - - it('Sends machine to incorrectAddress on correctAddress:false', async () => { - const params = {...defaultParams(), - currentSubConversation: defaultBillingConversation(), - poppedConversation: true - }; - - const { - conversationStack, - currentSubConversation, - sessionAttributes, - fallThrough, - pop, - } = await billing.acceptIntent(params); - - expect(conversationStack).toEqual([]); - expect(currentSubConversation).toEqual( - { - billing:{ - ...defaultBillingConversation().billing, - machineState:"incorrectAddress", - machineContext:{ - ...defaultBillingMachineContext(), - previousMachineState: "confirmAddress", - resuming: true - } - } - } - ); - expect(sessionAttributes).toEqual(defaultSessionAttributes()); - expect(fallThrough).toBeFalsy(); - expect(pop).toBeTruthy(); + handlerInput.requestEnvelope.request.type = 'LaunchRequest' + await run(handlerInput) + handlerInput.requestEnvelope.request.intent.name = 'Billing' + handlerInput.responseBuilder.speak.mockClear() + handlerInput.responseBuilder.reprompt.mockClear() + handlerInput.responseBuilder.addElicitSlotDirective.mockClear() + handlerInput.responseBuilder.addConfirmSlotDirective.mockClear() + handlerInput.responseBuilder.withShouldEndSession.mockClear() + handlerInput.responseBuilder.getResponse.mockClear() + handlerInput.requestEnvelope.request.type = 'IntentRequest' + mockGetSession.mockClear() + mockSaveSession.mockClear() + jest.spyOn(fetchBill, 'generateRandomBillAmount').mockImplementation(() => '$100.00') + jest.spyOn(fetchBill, 'fetchBill').mockImplementation(async () => Promise.resolve({ billAmount: '$100.00' })) + }) + + describe('routing logic', () => { + it('Sends machine to confirmAddress (yesNoQuestion) when new', async () => { + await run(handlerInput) + + expect(getMockState().machineState).toEqual('yesNoQuestion') + expect(getMockState().machineContext.alreadyAnswered(handlerInput.attributesManager.getSessionAttributes())).toBeFalsy(); + expect(getResponse()[0][0]).toEqual('confirmAddress.confirm') }) - it('Sends machine to reportBillToUser confirmedAddress true and correctAddress true', async () => { - const params = { - ...defaultParams(), - currentSubConversation: { - billing:{} - }, - sessionAttributes: { - ...defaultSessionAttributes(), - conversationAttributes:defaultConversationAttributes(true, true) - }, - intent: { - name:'test', - slots: {} - } - }; - - const { - conversationStack, - currentSubConversation, - sessionAttributes, - fallThrough, - pop, - } = await billing.acceptIntent(params); - - expect(conversationStack).toEqual([]); - expect(currentSubConversation).toEqual( - { - billing:{ - ...defaultBillingConversation().billing, - machineState:"reportBillToUser", - machineContext:{ - ...defaultBillingMachineContext(), - conversationAttributes:defaultConversationAttributes(true, true), - billAmount:currentSubConversation.billing.machineContext.billAmount, - previousMachineState: "fresh", - resuming: false - } - } - } - ); - expect(sessionAttributes).toEqual({ - ...defaultSessionAttributes(), - conversationAttributes:defaultConversationAttributes(true, true) - }); - expect(fallThrough).toBeFalsy(); - expect(pop).toBeTruthy(); - }); - - it('Sends machine to reportBillToUser confirmedAddress true and correctAddress true', async () => { - const params = { - ...defaultParams(), - currentSubConversation: { - billing:{} + it('Returns the user\'s bill when correctAddress: true', async () => { + handlerInput.attributesManager.setSessionAttributes({ + ...handlerInput.attributesManager.getSessionAttributes(), + state: { + ...handlerInput.attributesManager.getSessionAttributes().state, + currentSubConversation: { + billing: { + machineState: 'correctAddress', + machineContext: { + conversationAttributes: { + confirmAddress: { + confirmedAddress: true, + correctAddress: true, + }, + }, + }, + }, + }, + conversationStack: [], }, - sessionAttributes: { - ...defaultSessionAttributes(), - conversationAttributes:defaultConversationAttributes(true, false) + conversationAttributes: { + confirmAddress: { + confirmedAddress: true, + correctAddress: true, + }, }, - intent: { - name:'test', - slots: {} - } - }; + }) - const { - conversationStack, - currentSubConversation, - sessionAttributes, - fallThrough, - pop, - } = await billing.acceptIntent(params); + await run(handlerInput) - expect(conversationStack).toEqual([]); - expect(currentSubConversation).toEqual( - { - billing:{ - ...defaultBillingConversation().billing, - machineState:"incorrectAddress", - machineContext:{ - ...defaultBillingMachineContext(), - conversationAttributes:defaultConversationAttributes(true, false), - billAmount:currentSubConversation.billing.machineContext.billAmount, - previousMachineState: "fresh", - resuming: false - } - } - } - ); - expect(sessionAttributes).toEqual({ - ...defaultSessionAttributes(), - conversationAttributes:defaultConversationAttributes(true, false) - }); - expect(fallThrough).toBeFalsy(); - expect(pop).toBeTruthy(); - }); - }); - describe('craftResponse()', () => { - beforeEach(() => { - dialog.mockRestore(); - mockGetSession.mockClear() - mockSaveSession.mockClear() - }); + expect(getMockState().machineState).toEqual('returnBill') + expect(getResponse()[0][0]).toEqual('billing.returnBill') + }) - it('when address is incorrect, tells the user they must correct their address online or by phone to get an accurate bill report (badAddress)', () => { - const params = { - dialog, - subConversation: { - billing: { - machineState: 'incorrectAddress', - machineContext: {}, + it('Sends user to incorrectAddress when correctAddress: false', async () => { + handlerInput.attributesManager.setSessionAttributes({ + ...handlerInput.attributesManager.getSessionAttributes(), + state: { + ...handlerInput.attributesManager.getSessionAttributes().state, + currentSubConversation: { + billing: { + machineState: 'incorrectAddress', + machineContext: { + conversationAttributes: { + confirmAddress: { + confirmedAddress: true, + correctAddress: false, + resuming: true + }, + }, + }, + }, }, + conversationStack: [], }, - }; - - billing.craftResponse(params); - - expect(dialog.mock.calls[0][0]).toEqual('billing.wrongAddress'); - expect(dialog.mock.calls[0][1]).toEqual({ website: fakeWebsite, phoneNumber: fakePhoneNumber }); - }); - - it('when address is correct, tells the user their bill (reportBillToUser)', () => { - const params = { - dialog, - subConversation: { - billing: { - machineState: 'reportBillToUser', - machineContext: { billAmount: 'fake bill amount' }, + conversationAttributes: { + confirmAddress: { + confirmedAddress: true, + correctAddress: false, + resuming: true, }, }, - }; - - billing.craftResponse(params); - - expect(dialog.mock.calls[0][0]).toEqual('billing.returnBill'); - expect(dialog.mock.calls[0][1]).toEqual({ billAmount: 'fake bill amount' }); - }); + }) - it('gives the generic error response on errors (error)', () => { - const params = { - dialog, - subConversation: { - billing: { - machineState: 'error', - machineContext: { error: 'fake error' }, - }, - }, - }; + await run(handlerInput) - billing.craftResponse(params); - expect(dialog.mock.calls[0][0]).toEqual('home.error'); - }); - }); + expect(getMockState().machineState).toEqual('incorrectAddress') + expect(getResponse()[0][0]).toEqual('billing.incorrectAddress') + }) + }) describe('systemic test', () => { - beforeEach(() => { - dialog.mockRestore(); - handlerInput.requestEnvelope.request.type = 'IntentRequest'; - handlerInput.responseBuilder.speak.mockClear() - handlerInput.responseBuilder.reprompt.mockClear() - handlerInput.responseBuilder.addElicitSlotDirective.mockClear() - handlerInput.responseBuilder.addConfirmSlotDirective.mockClear() - handlerInput.responseBuilder.withShouldEndSession.mockClear() - handlerInput.responseBuilder.getResponse.mockClear() - mockGetSession.mockClear() - mockSaveSession.mockClear() - }); - it('walks through triggering uniqueness', async () => { - const requestAttributes = { - 'confirmAddress.confirm': "confirmAddress.confirm", - "confirmAddress.misheard": "confirmAddress.misheard", - "estimatedRestoration.homeOrOther.confirm": "estimatedRestoration.homeOrOther.confirm", - "estimatedRestoration.homeOrOther.misheard": "estimatedRestoration.homeOrOther.misheard", - "confirmAddress.resume": "confirmAddress.resume", - "billing.returnBill": "billing.returnBill", - "estimatedRestoration.reply.home": "estimatedRestoration.reply.home", - "home.reEngage": "home.reEngage", - } - handlerInput.attributesManager.setRequestAttributes(requestAttributes) handlerInput.attributesManager.setSessionAttributes({ - ...defaultSessionAttributes, + ...defaultSessionAttributes(), conversationAttributes:{ ...defaultConversationAttributes(false, false), }, state: { currentSubConversation: { - engagement: {} + billing: {} }, - conversationStack: [ - ], - } + conversationStack: [], + }, }) - const slots = {} - setSlots(slots) - - handlerInput.requestEnvelope.request.intent.name = 'Billing' - await StateHandler.handle(handlerInput) + await run(handlerInput) - expect(handlerInput.responseBuilder.speak.mock.calls.length).toEqual(1) - expect(handlerInput.responseBuilder.speak.mock.calls[0][0]).toEqual('confirmAddress.confirm'); + expect(getResponse().length).toEqual(1) + expect(getResponse()[0][0]).toEqual('confirmAddress.confirm'); handlerInput.responseBuilder.speak.mockClear(); - handlerInput.requestEnvelope.request.intent.name = 'EstimatedRestoration' - await StateHandler.handle(handlerInput) + // handlerInput.requestEnvelope.request.intent.name = 'EstimatedRestoration' + // await run(handlerInput) - expect(handlerInput.responseBuilder.speak.mock.calls.length).toEqual(1) - expect(handlerInput.responseBuilder.speak.mock.calls[0][0]).toEqual('estimatedRestoration.homeOrOther.confirm'); - handlerInput.responseBuilder.speak.mockClear(); + // expect(getResponse().length).toEqual(1) + // expect(getResponse[0][0]).toEqual('estimatedRestoration.homeOrOther.confirm'); + // handlerInput.responseBuilder.speak.mockClear(); - handlerInput.requestEnvelope.request.intent.name = 'Billing' - await StateHandler.handle(handlerInput) + // handlerInput.requestEnvelope.request.intent.name = 'Billing' + await run(handlerInput) expect(handlerInput.responseBuilder.speak.mock.calls.length).toEqual(1) expect(handlerInput.responseBuilder.speak.mock.calls[0][0]).toEqual('confirmAddress.misheard'); handlerInput.responseBuilder.speak.mockClear(); - handlerInput.requestEnvelope.request.intent.name = 'EstimatedRestoration' - await StateHandler.handle(handlerInput) + // handlerInput.requestEnvelope.request.intent.name = 'EstimatedRestoration' + // await run(handlerInput) - expect(handlerInput.responseBuilder.speak.mock.calls.length).toEqual(1) - expect(handlerInput.responseBuilder.speak.mock.calls[0][0]).toEqual('estimatedRestoration.homeOrOther.misheard'); - handlerInput.responseBuilder.speak.mockClear(); + // expect(handlerInput.responseBuilder.speak.mock.calls.length).toEqual(1) + // expect(handlerInput.responseBuilder.speak.mock.calls[0][0]).toEqual('estimatedRestoration.homeOrOther.misheard'); + // handlerInput.responseBuilder.speak.mockClear(); - handlerInput.requestEnvelope.request.intent.name = 'HomeIntent' - await StateHandler.handle(handlerInput) + // handlerInput.requestEnvelope.request.intent.name = 'HomeIntent' + // await run(handlerInput) - expect(handlerInput.responseBuilder.speak.mock.calls.length).toEqual(1) - expect(handlerInput.responseBuilder.speak.mock.calls[0][0]).toEqual('confirmAddress.confirm'); - handlerInput.responseBuilder.speak.mockClear(); + // expect(handlerInput.responseBuilder.speak.mock.calls.length).toEqual(1) + // expect(handlerInput.responseBuilder.speak.mock.calls[0][0]).toEqual('confirmAddress.confirm'); + // handlerInput.responseBuilder.speak.mockClear(); - handlerInput.requestEnvelope.request.intent.name = 'Billing' - await StateHandler.handle(handlerInput) + // handlerInput.requestEnvelope.request.intent.name = 'Billing' + await run(handlerInput) expect(handlerInput.responseBuilder.speak.mock.calls.length).toEqual(1) expect(handlerInput.responseBuilder.speak.mock.calls[0][0]).toEqual('confirmAddress.misheard'); @@ -379,10 +186,10 @@ describe('Billing Conversation Tests', () => { handlerInput.requestEnvelope.request.intent.name = 'YesNoIntent' setSlots(defaultYesNoIntent('yes').slots); - await StateHandler.handle(handlerInput) + await run(handlerInput) expect(handlerInput.responseBuilder.speak.mock.calls.length).toEqual(1) - expect(handlerInput.responseBuilder.speak.mock.calls[0][0]).toEqual('billing.returnBill estimatedRestoration.reply.home home.reEngage'); + expect(handlerInput.responseBuilder.speak.mock.calls[0][0]).toEqual('billing.returnBill'); handlerInput.responseBuilder.speak.mockClear(); //should be in home or other @@ -391,12 +198,20 @@ describe('Billing Conversation Tests', () => { expect(state.conversationStack).toEqual([]) expect(state.currentSubConversation).toEqual({ - engagement: { - machineState: 'resume', + billing: { + machineState: 'returnBill', machineContext: { + billAmount: '$324.91', + website: 'company.com', + phoneNumber: '314-333-3333', + previousMachineState: 'confirmAddress', resuming: true, - previousMachineState: "billing", - conversationAttributes: defaultConversationAttributes(true, true) + conversationAttributes: { + confirmAddress: { correctAddress: true, confirmedAddress: true }, + resume: { wipeConversation: false } + }, + error: '', + misunderstandingCount: 0 } } }) diff --git a/lambda/test/conversation/estimatedRestoration.test.js b/lambda/test/conversation/estimatedRestoration.test.js index 05b0f8e..322c2c3 100644 --- a/lambda/test/conversation/estimatedRestoration.test.js +++ b/lambda/test/conversation/estimatedRestoration.test.js @@ -23,18 +23,6 @@ const defaultEstimatedRestorationMachineContext = () => ({ describe('Estimated Restoration Conversation Tests', () => { beforeEach(async () => { - handlerInput.requestEnvelope.request.type = 'LaunchRequest'; - await run(handlerInput) - handlerInput.requestEnvelope.request.intent.name = 'EstimatedRestoration' - handlerInput.responseBuilder.speak.mockClear() - handlerInput.responseBuilder.reprompt.mockClear() - handlerInput.responseBuilder.addElicitSlotDirective.mockClear() - handlerInput.responseBuilder.addConfirmSlotDirective.mockClear() - handlerInput.responseBuilder.withShouldEndSession.mockClear() - handlerInput.responseBuilder.getResponse.mockClear() - handlerInput.requestEnvelope.request.type = 'IntentRequest'; - mockGetSession.mockClear() - mockSaveSession.mockClear() handlerInput.attributesManager.setRequestAttributes({ 'estimatedRestoration.address.wrongAddress': 'estimatedRestoration.address.wrongAddress', 'estimatedRestoration.homeOrOther.confirm': 'estimatedRestoration.homeOrOther.confirm', @@ -49,11 +37,25 @@ describe('Estimated Restoration Conversation Tests', () => { 'estimatedRestoration.goBack': 'estimatedRestoration.goBack', 'resume.fresh': 'resume.fresh', 'home.welcome': 'home.welcome', + 'home.engage': 'home.engage', 'home.promptResume': 'home.promptResume', 'home.misheardResume': 'home.misheardResume', 'estimatedRestoration.homeOrOther.confirm': 'estimatedRestoration.homeOrOther.confirm', 'estimatedRestoration.resume': 'estimatedRestoration.resume', + 'estimatedRestoration.otherLocation.misheard': 'estimateRestoration.otherLocation.misheard', }) + handlerInput.requestEnvelope.request.type = 'LaunchRequest'; + await run(handlerInput) + handlerInput.requestEnvelope.request.intent.name = 'EstimatedRestoration' + handlerInput.responseBuilder.speak.mockClear() + handlerInput.responseBuilder.reprompt.mockClear() + handlerInput.responseBuilder.addElicitSlotDirective.mockClear() + handlerInput.responseBuilder.addConfirmSlotDirective.mockClear() + handlerInput.responseBuilder.withShouldEndSession.mockClear() + handlerInput.responseBuilder.getResponse.mockClear() + handlerInput.requestEnvelope.request.type = 'IntentRequest'; + mockGetSession.mockClear() + mockSaveSession.mockClear() }); describe('routing logic', () => { @@ -170,7 +172,7 @@ describe('Estimated Restoration Conversation Tests', () => { describe('systemic test', () => { it('sends the machine to correctAddress after confirming address when user is asking about their home', async () => { handlerInput.attributesManager.setSessionAttributes({ - ...defaultSessionAttributes, + ...defaultSessionAttributes(), conversationAttributes: defaultConversationAttributes(true, true), state: { currentSubConversation: { @@ -292,13 +294,8 @@ describe('Estimated Restoration Conversation Tests', () => { conversationAttributes: {}, }) - console.log('before: ', handlerInput.attributesManager.getSessionAttributes()) - // setSlots({}) await run(handlerInput) - console.log('after: ', handlerInput.attributesManager.getSessionAttributes()) - - expect(handlerInput.responseBuilder.speak.mock.calls.length).toEqual(1) expect(handlerInput.responseBuilder.speak.mock.calls[0][0]).toEqual('estimatedRestoration.homeOrOther.confirm'); handlerInput.responseBuilder.speak.mockClear(); diff --git a/lambda/test/conversation/reportOutage.test.js b/lambda/test/conversation/reportOutage.test.js index 761616e..40c8e22 100644 --- a/lambda/test/conversation/reportOutage.test.js +++ b/lambda/test/conversation/reportOutage.test.js @@ -1,130 +1,139 @@ - -const { reportOutage } = require('../../conversation/reportOutage') -const { fakePhoneNumber, fakeWebsite } = require('../../constants') - -const dialog = jest.fn(); +const { + run, + mockGetSession, + mockSaveSession, + getMockState, + getResponse, + handlerInput, + setSlots, +} = require('../util/mocks') describe('Report Outage Conversation Tests', () => { - describe('acceptIntent()', () => { - it('Sends machine to askAboutAddress when new', async () => { - const params = { - conversationStack: [], - currentSubConversation: {reportOutage:{}}, - sessionAttributes: { - previousPoppedConversation: '' - }, - intent: { - name:'ReportOutage', - slots: {}, - }, - topConversation: true, - newConversation: false, - poppedConversation: false, - fallThrough: false, - }; - - const { - conversationStack, - currentSubConversation, - sessionAttributes, - fallThrough, - pop, - } = await reportOutage.acceptIntent(params); - - expect(conversationStack).toEqual([ - { - reportOutage:{ - machineState:"confirmAddress", - machineContext:{ - conversationAttributes:{}, - letThemKnow:"", - misunderstandingCount:0, - error:"", - previousMachineState: "fresh", - resuming: false - } - } - } - ]); - expect(currentSubConversation).toEqual( - { - confirmAddress: {} - } - ); - expect(sessionAttributes).toEqual( - { - conversationAttributes:{}, - previousPoppedConversation:'' - } - ); - expect(fallThrough).toBeFalsy(); - expect(pop).toBeFalsy(); + beforeEach(async () => { + handlerInput.attributesManager.setRequestAttributes({ + 'home.welcome': 'home.welcome', + 'home.engage': 'home.engage', + 'home.promptResume': 'home.promptResume', + 'home.error': 'home.error', + 'confirmAddress.confirm': 'confirmAddress.confirm', + 'confirmAddress.misheard': 'confirmAddress.misheard', + 'reportOutage.wrongAddress': 'reportOutage.wrongAddress', + 'reportOutage.reply.noContact': 'reportOutage.reply.noContact', + }) + handlerInput.requestEnvelope.request.type = 'LaunchRequest' + await run(handlerInput) + handlerInput.requestEnvelope.request.intent.name = 'ReportOutage' + handlerInput.responseBuilder.speak.mockClear() + handlerInput.responseBuilder.reprompt.mockClear() + handlerInput.responseBuilder.addElicitSlotDirective.mockClear() + handlerInput.responseBuilder.addConfirmSlotDirective.mockClear() + handlerInput.responseBuilder.withShouldEndSession.mockClear() + handlerInput.responseBuilder.getResponse.mockClear() + handlerInput.requestEnvelope.request.type = 'IntentRequest' + mockGetSession.mockClear() + mockSaveSession.mockClear() + }) + + describe('routing tests', () => { + it('Sends machine to confirmAddress when new', async () => { + await run(handlerInput) + + expect(getMockState().machineState).toEqual('yesNoQuestion') + expect(getResponse()[0][0]).toEqual('confirmAddress.confirm') }) - }); - - describe('craftResponse()', () => { - beforeEach(() => { - dialog.mockRestore(); - }); - - it('when address is incorrect, tells the user they must correct their address online or by phone to report outage (badAddress)', () => { - const params = { - dialog, - subConversation: { - reportOutage: { - machineState: 'incorrectAddress', - machineContext: {}, + it('when address is incorrect, tells the user they must correct their address online or by phone to report outage (badAddress)', async () => { + handlerInput.attributesManager.setSessionAttributes({ + ...handlerInput.attributesManager.getSessionAttributes(), + state: { + ...handlerInput.attributesManager.getSessionAttributes().state, + currentSubConversation: { + reportOutage: { + machineState: 'incorrectAddress', + machineContext: { + conversationAttributes: { + confirmAddress: { + confirmedAddress: true, + correctAddress: false, + resuming: true + }, + }, + }, + }, }, + conversationStack: [], }, - sessionAttributes: { - previousPoppedConversation: '' - } - }; + conversationAttributes: { + confirmAddress: { + confirmedAddress: true, + correctAddress: false, + resuming: true, + }, + }, + }) - reportOutage.craftResponse(params); + await run(handlerInput) - expect(dialog.mock.calls[0][0]).toEqual('reportOutage.wrongAddress'); - expect(dialog.mock.calls[0][1]).toEqual({ website: fakeWebsite, phoneNumber: fakePhoneNumber }); + expect(getMockState().machineState).toEqual('incorrectAddress') + expect(getResponse()[0][0]).toEqual('reportOutage.wrongAddress') }); - it('when address is correct, tells the user thanks for reporting(thanksForReporting)', () => { - const params = { - dialog, - subConversation: { - reportOutage: { - machineState: 'thanksForReporting', - machineContext: {}, + it('when address is correct, tells the user thanks for reporting(thanksForReporting)', async () => { + handlerInput.attributesManager.setSessionAttributes({ + ...handlerInput.attributesManager.getSessionAttributes(), + state: { + ...handlerInput.attributesManager.getSessionAttributes().state, + currentSubConversation: { + reportOutage: { + machineState: 'thanksForReporting', + machineContext: { + conversationAttributes: { + confirmAddress: { + confirmedAddress: true, + correctAddress: true, + resuming: true + }, + }, + }, + }, }, + conversationStack: [], }, - sessionAttributes: { - previousPoppedConversation: '' - } - }; + conversationAttributes: { + confirmAddress: { + confirmedAddress: true, + correctAddress: true, + resuming: true, + }, + }, + }) - reportOutage.craftResponse(params); + await run(handlerInput) - expect(dialog.mock.calls[0][0]).toEqual('reportOutage.reply.noContact'); + expect(getMockState().machineState).toEqual('thanksForReporting') + expect(getResponse()[0][0]).toEqual('reportOutage.reply.noContact') }); - it('gives the generic error response on errors (error)', () => { - const params = { - dialog, - subConversation: { - reportOutage: { - machineState: 'error', - machineContext: { error: 'fake error' }, + it('gives the generic error response on errors (error)', async () => { + handlerInput.attributesManager.setSessionAttributes({ + ...handlerInput.attributesManager.getSessionAttributes(), + state: { + ...handlerInput.attributesManager.getSessionAttributes().state, + currentSubConversation: { + reportOutage: { + machineState: 'error', + machineContext: { error: 'fake error' }, + }, }, - }, - sessionAttributes: { - previousPoppedConversation: '' + conversationStack: [], } - }; + }) - reportOutage.craftResponse(params); + await run(handlerInput) - expect(dialog.mock.calls[0][0]).toEqual('home.error'); + expect(getMockState().machineState).toEqual('error') + expect(getResponse()[0][0]).toEqual('home.error'); }); - }); + }) }); From a2456bbe8083b6639b3957e8b36902592e1c867f Mon Sep 17 00:00:00 2001 From: Oren Montano Date: Wed, 27 Apr 2022 13:46:19 -0500 Subject: [PATCH 09/10] improved samples --- lambda/conversation/reportOutage.js | 14 ++++---------- skill-package/interactionModels/custom/en-US.json | 8 +++++++- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/lambda/conversation/reportOutage.js b/lambda/conversation/reportOutage.js index 0329255..c265fb1 100644 --- a/lambda/conversation/reportOutage.js +++ b/lambda/conversation/reportOutage.js @@ -9,8 +9,8 @@ const { const { utils } = require('@ocelot-consulting/ocelot-voice-framework') const { fakePhoneNumber, fakeWebsite } = require('../constants') -const callOutageApi = async ({houseNumber, phoneNumber, attemptCount}) => { - console.log('outage reported', houseNumber, phoneNumber, attemptCount) +const callOutageApi = async ({houseNumber, phoneNumber}) => { + console.log('outage reported', houseNumber, phoneNumber) return [{ result: 'noOutage' @@ -21,18 +21,12 @@ const callOutageApi = async ({houseNumber, phoneNumber, attemptCount}) => { workDescription: 'Crews are on the scenen and expect repairs to complete in about an hour.' }, { result: 'badCombination' - }][attemptCount % 3] + }][houseNumber % 3] } const stateMap = { fresh: state( - transition('processIntent', 'askForHouseNumber', - guard(ctx => ctx.houseNumber === 0), - ), - transition('processIntent', 'askForTelephoneNumber', - guard(ctx => ctx.phoneNumber === ''), - ), - immediate('gotAllData'), + transition('processIntent', 'askForHouseNumber') ), askForHouseNumber: state( transition('processIntent', 'askForTelephoneNumber', diff --git a/skill-package/interactionModels/custom/en-US.json b/skill-package/interactionModels/custom/en-US.json index 2f8a739..5b1d484 100644 --- a/skill-package/interactionModels/custom/en-US.json +++ b/skill-package/interactionModels/custom/en-US.json @@ -159,6 +159,7 @@ "samples": [ "{number}", "house number {number}", + "the house number is {number}", "the number is {number}" ] }, @@ -175,8 +176,13 @@ ], "samples": [ "{phoneNumber}", + "phone number is {phoneNumber}", + "area code {phoneNumber}", "my number is {phoneNumber}", - "my phone number is {phoneNumber}" + "my phone number is {phoneNumber}", + "my telephone number is {phoneNumber}", + "the phone number is {phoneNumber}", + "the telephone number is {phoneNumber}" ] }, { From 10591469802921aca1ba36bf48b7b246c7517f83 Mon Sep 17 00:00:00 2001 From: Lucas Date: Wed, 27 Apr 2022 16:33:05 -0500 Subject: [PATCH 10/10] all passing --- lambda/conversation/billing.js | 1 + lambda/conversation/estimatedRestoration.js | 1 + lambda/conversation/reportOutage.js | 1 + lambda/package-lock.json | 32 ++++++------ lambda/package.json | 2 +- lambda/test/conversation/billing.test.js | 58 +++++++++++---------- 6 files changed, 50 insertions(+), 45 deletions(-) diff --git a/lambda/conversation/billing.js b/lambda/conversation/billing.js index dfb1c71..b727c6e 100644 --- a/lambda/conversation/billing.js +++ b/lambda/conversation/billing.js @@ -56,6 +56,7 @@ const billing = { }), intent: 'Billing', canInterrupt: true, + shouldBeUnique: true, } module.exports = { billing } diff --git a/lambda/conversation/estimatedRestoration.js b/lambda/conversation/estimatedRestoration.js index 1fa1326..09969f6 100644 --- a/lambda/conversation/estimatedRestoration.js +++ b/lambda/conversation/estimatedRestoration.js @@ -107,6 +107,7 @@ const estimatedRestoration = { intent: 'EstimatedRestoration', canInterrupt: true, description: 'how long it will take to restore power', + shouldBeUnique: true, } module.exports = { estimatedRestoration } diff --git a/lambda/conversation/reportOutage.js b/lambda/conversation/reportOutage.js index a011d8e..fbc62d2 100644 --- a/lambda/conversation/reportOutage.js +++ b/lambda/conversation/reportOutage.js @@ -80,6 +80,7 @@ const reportOutage = { }), intent: 'ReportOutage', canInterrupt: true, + shouldBeUnique: true, } module.exports = { reportOutage } diff --git a/lambda/package-lock.json b/lambda/package-lock.json index 8e37db4..dadff2d 100644 --- a/lambda/package-lock.json +++ b/lambda/package-lock.json @@ -385,9 +385,9 @@ } }, "@babel/runtime": { - "version": "7.17.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.2.tgz", - "integrity": "sha512-hzeyJyMA1YGdJTuWU0e/j4wKXrU4OMFvY2MSlaI9B7VQb0r5cxTE3EAIS2Q7Tn2RIcDkRvTA/v2JsAEhxe99uw==", + "version": "7.17.9", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.9.tgz", + "integrity": "sha512-lSiBBvodq29uShpWGNbgFdKYNiFDo5/HIYsaCEY9ff4sb10x9jizo2+pRrSyF4jKZCXqgzuqBOQKbUm90gQwJg==", "requires": { "regenerator-runtime": "^0.13.4" } @@ -747,9 +747,9 @@ } }, "@ocelot-consulting/ocelot-voice-framework": { - "version": "1.1.75", - "resolved": "https://registry.npmjs.org/@ocelot-consulting/ocelot-voice-framework/-/ocelot-voice-framework-1.1.75.tgz", - "integrity": "sha512-sp1UBIkQ1pZm2YbDsZE/8dxrIzyFYqGDYZOZCXeSzGnebv7utwWiu1bPm/fv0FOXixr75V6XmtncPW1hvGc9MQ==", + "version": "1.1.84", + "resolved": "https://registry.npmjs.org/@ocelot-consulting/ocelot-voice-framework/-/ocelot-voice-framework-1.1.84.tgz", + "integrity": "sha512-oeQLOlFlzn0d4kcchii9QMm8fM6NvFRPvHk1mxsy3d0drdFAt3Izi7axLEexJXzxXHpqCHkmjYzQKPIQTbGzUw==", "requires": { "ask-sdk-core": "^2.12.0", "ask-sdk-model": "^1.37.2", @@ -1002,17 +1002,17 @@ "dev": true }, "ask-sdk-core": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/ask-sdk-core/-/ask-sdk-core-2.12.0.tgz", - "integrity": "sha512-K+jnoEgWL0vCvWdnw0ioQTszdZrRWv9FN1SzxOM+zc9kFMqXc4kAv/eCHPMi4rExMAztyJXbRStfBL/Morv/PA==", + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/ask-sdk-core/-/ask-sdk-core-2.12.1.tgz", + "integrity": "sha512-2fT7XkHfuI0I6g12Y4mztgzmz7SwrKli8Z2p+eut1dVk8t5BHxWVXZCOVI5GVhW0DJbWwhjPSfOTAZX+zPoKEA==", "requires": { "ask-sdk-runtime": "^2.12.0" } }, "ask-sdk-model": { - "version": "1.37.2", - "resolved": "https://registry.npmjs.org/ask-sdk-model/-/ask-sdk-model-1.37.2.tgz", - "integrity": "sha512-Fh0fgnxwJE3qBi5lqgbrAudeuycxUU8LwaTiIKceq+seKsA3SWAa+5cpnF6pslkwTekRc80jpiq0FaDnzF4Qsw==" + "version": "1.38.1", + "resolved": "https://registry.npmjs.org/ask-sdk-model/-/ask-sdk-model-1.38.1.tgz", + "integrity": "sha512-I+Pxek0ZdWWGxAbsyipFM/7v43zokxfesHMsQ+vGRuk4S4bKREmy9g6CJKm2EtLC3ytB2ZalVEAm1THzeWNQ9w==" }, "ask-sdk-runtime": { "version": "2.12.0", @@ -1921,11 +1921,11 @@ "dev": true }, "i18next": { - "version": "21.6.12", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-21.6.12.tgz", - "integrity": "sha512-xlGTPdu2g5PZEUIE6TA1mQ9EIAAv9nMFONzgwAIrKL/KTmYYWufQNGgOmp5Og1PvgUji+6i1whz0rMdsz1qaKw==", + "version": "21.6.16", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-21.6.16.tgz", + "integrity": "sha512-xJlzrVxG9CyAGsbMP1aKuiNr1Ed2m36KiTB7hjGMG2Zo4idfw3p9THUEu+GjBwIgEZ7F11ZbCzJcfv4uyfKNuw==", "requires": { - "@babel/runtime": "^7.12.0" + "@babel/runtime": "^7.17.2" } }, "i18next-sprintf-postprocessor": { diff --git a/lambda/package.json b/lambda/package.json index cf7b140..b3bc895 100644 --- a/lambda/package.json +++ b/lambda/package.json @@ -10,7 +10,7 @@ "author": "Ocelot Consulting", "license": "ISC", "dependencies": { - "@ocelot-consulting/ocelot-voice-framework": "^1.1.74", + "@ocelot-consulting/ocelot-voice-framework": "^1.1.84", "robot3": "^0.2.21" }, "devDependencies": { diff --git a/lambda/test/conversation/billing.test.js b/lambda/test/conversation/billing.test.js index 425d186..6c61268 100644 --- a/lambda/test/conversation/billing.test.js +++ b/lambda/test/conversation/billing.test.js @@ -1,3 +1,7 @@ +const fetchBill = require('../../service/fetchBill') +jest.spyOn(fetchBill, 'generateRandomBillAmount').mockImplementation(() => '$100.00') +jest.spyOn(fetchBill, 'fetchBill').mockImplementation(async () => ({ billAmount: '$100.00' })) + const { run, mockGetSession, @@ -12,7 +16,6 @@ const { defaultSessionAttributes, defaultYesNoIntent, } = require('../util/defaults') -const fetchBill = require('../../service/fetchBill') describe('Billing Conversation Tests', () => { beforeEach(async () => { @@ -29,6 +32,7 @@ describe('Billing Conversation Tests', () => { 'estimatedRestoration.reply.home': 'estimatedRestoration.reply.home', 'home.reEngage': 'home.reEngage', 'home.promptResume': 'home.promptResume', + 'home.misheardResume': 'home.misheardResume', }) handlerInput.requestEnvelope.request.type = 'LaunchRequest' await run(handlerInput) @@ -42,8 +46,6 @@ describe('Billing Conversation Tests', () => { handlerInput.requestEnvelope.request.type = 'IntentRequest' mockGetSession.mockClear() mockSaveSession.mockClear() - jest.spyOn(fetchBill, 'generateRandomBillAmount').mockImplementation(() => '$100.00') - jest.spyOn(fetchBill, 'fetchBill').mockImplementation(async () => Promise.resolve({ billAmount: '$100.00' })) }) describe('routing logic', () => { @@ -139,7 +141,10 @@ describe('Billing Conversation Tests', () => { currentSubConversation: { billing: {} }, - conversationStack: [], + conversationStack: [{ home: { + machineState: 'billing', + machineContext: {}, + } }], }, }) @@ -149,35 +154,35 @@ describe('Billing Conversation Tests', () => { expect(getResponse()[0][0]).toEqual('confirmAddress.confirm'); handlerInput.responseBuilder.speak.mockClear(); - // handlerInput.requestEnvelope.request.intent.name = 'EstimatedRestoration' - // await run(handlerInput) + handlerInput.requestEnvelope.request.intent.name = 'EstimatedRestoration' + await run(handlerInput) - // expect(getResponse().length).toEqual(1) - // expect(getResponse[0][0]).toEqual('estimatedRestoration.homeOrOther.confirm'); - // handlerInput.responseBuilder.speak.mockClear(); + expect(getResponse().length).toEqual(1) + expect(getResponse()[0][0]).toEqual('estimatedRestoration.homeOrOther.confirm'); + handlerInput.responseBuilder.speak.mockClear(); - // handlerInput.requestEnvelope.request.intent.name = 'Billing' + handlerInput.requestEnvelope.request.intent.name = 'Billing' await run(handlerInput) expect(handlerInput.responseBuilder.speak.mock.calls.length).toEqual(1) expect(handlerInput.responseBuilder.speak.mock.calls[0][0]).toEqual('confirmAddress.misheard'); handlerInput.responseBuilder.speak.mockClear(); - // handlerInput.requestEnvelope.request.intent.name = 'EstimatedRestoration' - // await run(handlerInput) + handlerInput.requestEnvelope.request.intent.name = 'EstimatedRestoration' + await run(handlerInput) - // expect(handlerInput.responseBuilder.speak.mock.calls.length).toEqual(1) - // expect(handlerInput.responseBuilder.speak.mock.calls[0][0]).toEqual('estimatedRestoration.homeOrOther.misheard'); - // handlerInput.responseBuilder.speak.mockClear(); + expect(handlerInput.responseBuilder.speak.mock.calls.length).toEqual(1) + expect(handlerInput.responseBuilder.speak.mock.calls[0][0]).toEqual('estimatedRestoration.homeOrOther.misheard'); + handlerInput.responseBuilder.speak.mockClear(); - // handlerInput.requestEnvelope.request.intent.name = 'HomeIntent' - // await run(handlerInput) + handlerInput.requestEnvelope.request.intent.name = 'HomeIntent' + await run(handlerInput) - // expect(handlerInput.responseBuilder.speak.mock.calls.length).toEqual(1) - // expect(handlerInput.responseBuilder.speak.mock.calls[0][0]).toEqual('confirmAddress.confirm'); - // handlerInput.responseBuilder.speak.mockClear(); + expect(handlerInput.responseBuilder.speak.mock.calls.length).toEqual(1) + expect(handlerInput.responseBuilder.speak.mock.calls[0][0]).toEqual('confirmAddress.confirm'); + handlerInput.responseBuilder.speak.mockClear(); - // handlerInput.requestEnvelope.request.intent.name = 'Billing' + handlerInput.requestEnvelope.request.intent.name = 'Billing' await run(handlerInput) expect(handlerInput.responseBuilder.speak.mock.calls.length).toEqual(1) @@ -189,7 +194,7 @@ describe('Billing Conversation Tests', () => { await run(handlerInput) expect(handlerInput.responseBuilder.speak.mock.calls.length).toEqual(1) - expect(handlerInput.responseBuilder.speak.mock.calls[0][0]).toEqual('billing.returnBill'); + expect(handlerInput.responseBuilder.speak.mock.calls[0][0]).toEqual('billing.returnBill confirmAddress.resume estimatedRestoration.reply.home home.reEngage'); handlerInput.responseBuilder.speak.mockClear(); //should be in home or other @@ -198,13 +203,10 @@ describe('Billing Conversation Tests', () => { expect(state.conversationStack).toEqual([]) expect(state.currentSubConversation).toEqual({ - billing: { - machineState: 'returnBill', + home: { + machineState: 'resume', machineContext: { - billAmount: '$324.91', - website: 'company.com', - phoneNumber: '314-333-3333', - previousMachineState: 'confirmAddress', + previousMachineState: 'billing', resuming: true, conversationAttributes: { confirmAddress: { correctAddress: true, confirmedAddress: true },