diff --git a/lambda/conversation/billing.js b/lambda/conversation/billing.js index 994e4a6..b727c6e 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 })), ), @@ -69,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 8b44fdc..09969f6 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) } ), @@ -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..eef360d 100644 --- a/lambda/conversation/reportOutage.js +++ b/lambda/conversation/reportOutage.js @@ -9,38 +9,73 @@ 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}) => { + console.log('outage reported', houseNumber, phoneNumber) + + 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' + }][houseNumber % 3] } const stateMap = { fresh: state( - transition('processIntent', 'confirmAddress', - guard(({ conversationAttributes }) => !conversationAttributes.confirmAddress?.confirmedAddress), + transition('processIntent', 'askForHouseNumber') + ), + askForHouseNumber: state( + transition('processIntent', 'askForTelephoneNumber', + guard((ctx, { intent }) => intent.name === 'ANumber'), + reduce((ctx, { intent } ) => ({ ...ctx, houseNumber: intent.slots.number.value, misunderstandingCount: 0 })) ), - transition('processIntent', 'incorrectAddress', - guard(({ conversationAttributes }) => !conversationAttributes.confirmAddress?.correctAddress), + transition('processIntent', 'goBack', + guard(({ misunderstandingCount }, { intent }) => intent.name === 'GoBackIntent' || misunderstandingCount > 3) ), - transition('processIntent', 'letYouKnow', - guard(({ conversationAttributes }) => conversationAttributes.confirmAddress?.correctAddress), + 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 +84,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,22 +139,32 @@ 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, + shouldBeUnique: true, } module.exports = { reportOutage } 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.` ] } } 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/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 new file mode 100644 index 0000000..6c61268 --- /dev/null +++ b/lambda/test/conversation/billing.test.js @@ -0,0 +1,222 @@ +const fetchBill = require('../../service/fetchBill') +jest.spyOn(fetchBill, 'generateRandomBillAmount').mockImplementation(() => '$100.00') +jest.spyOn(fetchBill, 'fetchBill').mockImplementation(async () => ({ billAmount: '$100.00' })) + +const { + run, + mockGetSession, + mockSaveSession, + getMockState, + getResponse, + handlerInput, + setSlots, +} = require('../util/mocks') +const { + defaultConversationAttributes, + defaultSessionAttributes, + defaultYesNoIntent, +} = require('../util/defaults') + +describe('Billing Conversation Tests', () => { + 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', + 'home.misheardResume': 'home.misheardResume', + }) + 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() + }) + + 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('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: [], + }, + conversationAttributes: { + confirmAddress: { + confirmedAddress: true, + correctAddress: true, + }, + }, + }) + + await run(handlerInput) + + + expect(getMockState().machineState).toEqual('returnBill') + expect(getResponse()[0][0]).toEqual('billing.returnBill') + }) + + 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: [], + }, + conversationAttributes: { + confirmAddress: { + confirmedAddress: true, + correctAddress: false, + resuming: true, + }, + }, + }) + + await run(handlerInput) + + + expect(getMockState().machineState).toEqual('incorrectAddress') + expect(getResponse()[0][0]).toEqual('billing.incorrectAddress') + }) + }) + + describe('systemic test', () => { + it('walks through triggering uniqueness', async () => { + handlerInput.attributesManager.setSessionAttributes({ + ...defaultSessionAttributes(), + conversationAttributes:{ + ...defaultConversationAttributes(false, false), + }, + state: { + currentSubConversation: { + billing: {} + }, + conversationStack: [{ home: { + machineState: 'billing', + machineContext: {}, + } }], + }, + }) + + await run(handlerInput) + + expect(getResponse().length).toEqual(1) + expect(getResponse()[0][0]).toEqual('confirmAddress.confirm'); + handlerInput.responseBuilder.speak.mockClear(); + + 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(); + + 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) + + 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) + + 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 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 = 'YesNoIntent' + setSlots(defaultYesNoIntent('yes').slots); + await run(handlerInput) + + expect(handlerInput.responseBuilder.speak.mock.calls.length).toEqual(1) + 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 + + const { state } = handlerInput.attributesManager.getSessionAttributes() + + expect(state.conversationStack).toEqual([]) + expect(state.currentSubConversation).toEqual({ + home: { + machineState: 'resume', + machineContext: { + previousMachineState: 'billing', + resuming: 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 new file mode 100644 index 0000000..322c2c3 --- /dev/null +++ b/lambda/test/conversation/estimatedRestoration.test.js @@ -0,0 +1,409 @@ +const { defaultSessionAttributes, defaultConversationAttributes, defaultYesNoIntent, defaultPickALetterIntent } = require('../util/defaults'); +const { + run, + mockGetSession, + mockSaveSession, + getMockState, + getResponse, + handlerInput, + setSlots, +} = require('../util/mocks') + +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', () => { + beforeEach(async () => { + 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.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', () => { + it('Sends machine to askAboutAddress when new', async () => { + await run(handlerInput) + + 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 () => { + handlerInput.requestEnvelope.request.intent.name = 'OtherIntent' + 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') + }) + + 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: [] + }, + conversationAttributes: { + confirmAddress: { + correctAddress: false, + }, + }, + }) + handlerInput.requestEnvelope.request.intent.name = 'EstimatedRestoration' + + await run(handlerInput); + + 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 () => { + handlerInput.attributesManager.setSessionAttributes({ + ...handlerInput.attributesManager.getSessionAttributes(), + state: { + ...handlerInput.attributesManager.getSessionAttributes().state, + currentSubConversation: { + estimatedRestoration: { + machineState: 'reportEstimateToUser', + machineContext: { + conversationAttributes: { + confirmAddress: { + correctAddress: true, + } + }, + estimate: '6 hours', + }, + }, + }, + conversationStack: [], + }, + conversationAttributes: { + conversationAttributes: { + confirmAddress: { + correctAddress: true, + } + } + } + }) + + await run(handlerInput); + + expect(getMockState().machineState).toEqual('reportEstimateToUser') + expect(getResponse()[0][0]).toEqual('estimatedRestoration.reply.other'); + }); + + it('gives the generic error response on errors (error)', async () => { + handlerInput.attributesManager.setSessionAttributes({ + ...handlerInput.attributesManager.getSessionAttributes(), + state: { + ...handlerInput.attributesManager.getSessionAttributes().state, + currentSubConversation: { + estimatedRestoration: { + machineState: 'error', + machineContext: { error: 'fake error' }, + }, + }, + conversationStack: [], + } + }) + + await run(handlerInput); + + 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 () => { + 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(), + } + }], + } + }) + + setSlots({}) + handlerInput.requestEnvelope.request.intent.name = 'HomeIntent' + + await run(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(getResponse().length).toEqual(1) + }); + + it('sends the machine to incorrectAddress after confirming address when user is asking about their home', async () => { + 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() + } + } + } + ], + } + }) + + setSlots({}) + handlerInput.requestEnvelope.request.intent.name = 'HomeIntent' + + await run(handlerInput) + + const { state } = handlerInput.attributesManager.getSessionAttributes() + + expect(state.currentSubConversation).toEqual({ + estimatedRestoration: { + machineState: 'incorrectAddress', + machineContext: { + ...defaultEstimatedRestorationMachineContext(), + resuming: true, + previousMachineState: 'confirmAddress', + conversationAttributes: defaultConversationAttributes(true), + } + } + }) + expect(getResponse().length).toEqual(1) + }); + + it('walks through the expected path of estimatedRestoration', async () => { + handlerInput.attributesManager.setSessionAttributes({ + ...handlerInput.attributesManager.getSessionAttributes(), + state: { + ...handlerInput.attributesManager.getSessionAttributes().state, + currentSubConversation: { estimatedRestoration: {}}, + conversationStack: [], + }, + conversationAttributes: {}, + }) + + 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 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(); + + handlerInput.requestEnvelope.request.intent.name = 'YesNoIntent' + setSlots(defaultYesNoIntent('yes').slots); + await run(handlerInput) + + expect(handlerInput.responseBuilder.speak.mock.calls.length).toEqual(1) + expect(handlerInput.responseBuilder.speak.mock.calls[0][0]).toEqual('estimatedRestoration.reply.home'); + handlerInput.responseBuilder.speak.mockClear(); + + handlerInput.requestEnvelope.request.intent.name = 'EstimatedRestoration' + + // 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.reply.home'); + handlerInput.responseBuilder.speak.mockClear(); + + const { state } = handlerInput.attributesManager.getSessionAttributes() + + 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' + + 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 run(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 run(handlerInput) + + expect(handlerInput.responseBuilder.speak.mock.calls.length).toEqual(1) + 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({ + estimatedRestoration: { + machineState: 'reportEstimateToUser', + machineContext: { + selectedHome: false, + selectedOutage: '', + estimate: '3 hours', + misunderstandingCount: 0, + error: '', + previousMachineState: 'pickAnOutage', + resuming: true, + conversationAttributes: {}, + address: '123 Company Dr' + } + } + }) + }); + }); +}); + diff --git a/lambda/test/conversation/reportOutage.test.js b/lambda/test/conversation/reportOutage.test.js new file mode 100644 index 0000000..40c8e22 --- /dev/null +++ b/lambda/test/conversation/reportOutage.test.js @@ -0,0 +1,139 @@ +const { + run, + mockGetSession, + mockSaveSession, + getMockState, + getResponse, + handlerInput, + setSlots, +} = require('../util/mocks') + +describe('Report Outage Conversation Tests', () => { + 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') + }) + + + 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: [], + }, + conversationAttributes: { + confirmAddress: { + confirmedAddress: true, + correctAddress: false, + resuming: true, + }, + }, + }) + + await run(handlerInput) + + expect(getMockState().machineState).toEqual('incorrectAddress') + expect(getResponse()[0][0]).toEqual('reportOutage.wrongAddress') + }); + + 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: [], + }, + conversationAttributes: { + confirmAddress: { + confirmedAddress: true, + correctAddress: true, + resuming: true, + }, + }, + }) + + await run(handlerInput) + + expect(getMockState().machineState).toEqual('thanksForReporting') + expect(getResponse()[0][0]).toEqual('reportOutage.reply.noContact') + }); + + 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' }, + }, + }, + conversationStack: [], + } + }) + + await run(handlerInput) + + expect(getMockState().machineState).toEqual('error') + expect(getResponse()[0][0]).toEqual('home.error'); + }); + }) +}); 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/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/mocks.js b/lambda/test/util/mocks.js new file mode 100644 index 0000000..466e913 --- /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(x => console.log('this is x: ', x)).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 getResponse = () => handlerInput.responseBuilder.speak.mock.calls + +const { StateHandler } = initialize({ + conversationSet, + dialog, + fetchSession: mockGetSession, + saveSession: mockSaveSession, +}) + +module.exports = { + run: async input => await StateHandler.handle(input), + mockGetSession, + mockSaveSession, + getMockState, + getResponse, + handlerInput, + setSlots, +} diff --git a/skill-package/interactionModels/custom/en-US.json b/skill-package/interactionModels/custom/en-US.json index 1997685..5b1d484 100644 --- a/skill-package/interactionModels/custom/en-US.json +++ b/skill-package/interactionModels/custom/en-US.json @@ -145,6 +145,46 @@ "estimated restoration time" ] }, + { + "name": "ANumber", + "slots": [ + { + "name": "number", + "type": "AMAZON.NUMBER", + "samples": [ + "{number}" + ] + } + ], + "samples": [ + "{number}", + "house number {number}", + "the house number is {number}", + "the number is {number}" + ] + }, + { + "name": "APhoneNumber", + "slots": [ + { + "name": "phoneNumber", + "type": "AMAZON.PhoneNumber", + "samples": [ + "{phoneNumber}" + ] + } + ], + "samples": [ + "{phoneNumber}", + "phone number is {phoneNumber}", + "area code {phoneNumber}", + "my number is {phoneNumber}", + "my phone number is {phoneNumber}", + "my telephone number is {phoneNumber}", + "the phone number is {phoneNumber}", + "the telephone number is {phoneNumber}" + ] + }, { "name": "ReportOutage", "slots": [ @@ -302,6 +342,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": [],