0

I am implementing end-to-end tests for a Twilio Autopilot bot. I start the conversation with the bot using a custom chat channel, by making an HTTP POST to this URL: https://channels.autopilot.twilio.com/v2/AC8xxxxxxxxxxxxxx/UA6xxxxxxxxxxxxx/custom/chat

I assume that underneath, this creates a new chat channel named chat and adds the user to this channel, then starts a new Dialogue.

This approach is described in the Twilio documentation here: https://www.twilio.com/docs/autopilot/channels/custom-channel

The response I get includes a dialogue SID:

 "dialogue": {
    "sid": "UK32f8xxxxxxxxxxxxxxxxxx",
    ...
 }

Since I want the tests to be repeatable with the same results, I want to be able to programmatically stop the Dialogue that was started at the end of each test.

Is there a way to programmatically stop the Dialogue, given the dialogue SID? Is there a way to programmatically delete the custom channel if the Dialogue cannot be stopped? Other ways to resolve this problem are also very welcome. Thank you.

Daniel Gabriel
  • 3,939
  • 2
  • 26
  • 37

1 Answers1

0

Though this does not answer "how to stop the dialogue" or "how to delete the custom channel", this is the solution I used to resolve the 'repeatable tests' requirement.

I'm using Node and axios, though the code below is mostly a simple HTTP request, so it could be replicated in other languages.

The helper class is pretty self-explanatory - it just generates a new user id on creation to avoid conversation collisions, then makes a call to the custom channel URL mentioned in my question:

export class AutopilotUtils {
    private authHeaderValue: string;
    private userIdentity: string;
    private targetUrl: string;
    
    constructor(
        twilioAccountSid: string,
        twilioAuthToken: string,
        twilioAssistandSid: string) {

        const autopilotUrl = `https://channels.autopilot.twilio.com/v1/${twilioAccountSid}/${twilioAssistantSid}/custom/testchat`;

        // the Inbound Context becomes the value of the Memory parameter
        // on the target URL
        const inboundContext = {
            someProp: "somePropValue"
        };
        this.userIdentity = `testUser_${this.generateQuickGuid()}`;
        this.authHeaderValue = `Basic ${Buffer.from(twilioAccountSid + ":" + twilioAuthToken).toString("base64")}`;
        this.targetUrl = `${autopilotUrl}?Memory=${JSON.stringify(inboundContext)}`;
    }

    public async sendMessage(userInput: string): Promise<AxiosResponse<any>> {
        const result = await axios({
            method: "post",
            url: this.targetUrl,
            data: `user_id=${this.userIdentity}&text=${userInput}`,
            headers: {
                "Content-Type": "application/x-www-form-urlencoded",
                "Accept": "application/json",
                Authorization: this.authHeaderValue
            }
        });
        return result;
    }

    private generateQuickGuid() {
        return Math.random().toString(36).substring(2, 15) +
            Math.random().toString(36).substring(2, 15);
    }
}

Then it can be used in a test like this (using jest and chai):

// make a new utils instance (which will use the same user identity
// for the duration of its lifetime)
const utils = new AutopilotUtils(acctSid, authToken, botSid);

// send "hello" to the bot
const result = await utils.sendMessage("hello");

// result will contain the actions that were returned. for v1, the
// shape of the returned object is:
//
// { says: [{speech: "blah"}, {speech: "blah again"}], listen: {}}
//
// the action names, order, and content will match what the bot
// returns, so every action might have its own shape and would
// need to be checked accordingly. we are checking the "data"
// property only because that's how axios data comes back, it's
// not part of the object shape returned from Twilio.
expect(result).to.not.be.undefined;
expect(result).to.have.property("data").that.has.property("says");
const responseArray = result.data["says"];
expect(responseArray).to
    .satisfy((s: {speech: string}[]) => s.some(i => i.speech && i.speech.includes("hi there")));

The comments in the code explain how it works.

Daniel Gabriel
  • 3,939
  • 2
  • 26
  • 37