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.