Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add trace activity-buffering logic when DeliveryMode is "expectReplies" #2738

Merged
merged 8 commits into from
Oct 23, 2020
4 changes: 2 additions & 2 deletions libraries/botbuilder-core/src/turnContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -503,8 +503,8 @@ export class TurnContext {
// Append activities to buffer
const responses: ResourceResponse[] = [];
output.forEach((a) => {
this.bufferedReplyActivities.push(a);
responses.push({ id: undefined });
this.bufferedReplyActivities.push(a);
responses.push({ id: undefined });
});

// Set responded flag
Expand Down
17 changes: 11 additions & 6 deletions libraries/botbuilder-core/tests/turnContext.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
const assert = require('assert');
const { ActivityTypes, BotAdapter, DeliveryModes, MessageFactory, TurnContext } = require('../');
const {
ActivityTypes,
BotAdapter,
DeliveryModes,
MessageFactory,
TurnContext
} = require('../');

const activityId = `activity ID`;

Expand Down Expand Up @@ -466,18 +472,17 @@ describe(`TurnContext`, function () {
activity.deliveryMode = DeliveryModes.ExpectReplies;
const context = new TurnContext(new SimpleAdapter(), activity);

const activities = [ MessageFactory.text('test'), MessageFactory.text('second test') ];
const activities = [MessageFactory.text('test'), MessageFactory.text('second test')];
const responses = await context.sendActivities(activities);

assert.strictEqual(responses.length, 2);

// For expectReplies all ResourceResponses should have no id.
const ids = responses.filter(response => response.id === undefined);
assert.strictEqual(ids.length, 2);
assert(responses.every(response => response.id === undefined));

const replies = context.bufferedReplyActivities;
assert.strictEqual(replies.length, 2);
assert.strictEqual(replies[0].text, 'test');
assert.strictEqual(replies[1].text, 'second test');
})
});
});
16 changes: 12 additions & 4 deletions libraries/botbuilder/src/botFrameworkAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
Activity,
ActivityTypes,
CallerIdConstants,
Channels,
CoreAppCredentials,
BotAdapter,
BotCallbackHandlerKey,
Expand Down Expand Up @@ -1195,8 +1196,15 @@ export class BotFrameworkAdapter
if (request.deliveryMode === DeliveryModes.ExpectReplies) {
// Handle "expectReplies" scenarios where all the activities have been buffered and sent back at once
// in an invoke response.
const expectedReplies: ExpectedReplies = { activities: context.bufferedReplyActivities as Activity[] };
body = expectedReplies;
let activities = context.bufferedReplyActivities as Activity[];

// If the channel is not the emulator, do not send trace activities.
// Fixes: https://github.com/microsoft/botbuilder-js/issues/2732
if (request.channelId !== Channels.Emulator) {
activities = activities.filter((a) => a.type !== ActivityTypes.Trace);
}

body = { activities } as ExpectedReplies;
status = StatusCodes.OK;
} else if (request.type === ActivityTypes.Invoke) {
// Retrieve a cached Invoke response to handle Invoke scenarios.
Expand Down Expand Up @@ -1331,8 +1339,8 @@ export class BotFrameworkAdapter
TokenResolver.checkForOAuthCards(this, context, activity as Activity);
}
const client = this.getOrCreateConnectorClient(context, activity.serviceUrl, this.credentials);
if (activity.type === 'trace' && activity.channelId !== 'emulator') {
// Just eat activity
if (activity.type === ActivityTypes.Trace && activity.channelId !== Channels.Emulator) {
// Just eat activity
responses.push({} as ResourceResponse);
} else if (activity.replyToId) {
responses.push(
Expand Down
60 changes: 59 additions & 1 deletion libraries/botbuilder/tests/botFrameworkAdapter.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
const assert = require('assert');
const { ActivityHandler, ActivityTypes, CallerIdConstants, DeliveryModes, StatusCodes, TurnContext } = require('botbuilder-core');
const {
ActivityHandler,
ActivityTypes,
CallerIdConstants,
Channels,
DeliveryModes,
StatusCodes,
TurnContext
} = require('botbuilder-core');
const connector = require('botframework-connector');
const {
AuthenticationConstants,
Expand Down Expand Up @@ -35,6 +43,13 @@ const incomingMessage = TurnContext.applyConversationReference({ text: 'test', t
const outgoingMessage = TurnContext.applyConversationReference({ text: 'test', type: 'message' }, reference);
const incomingInvoke = TurnContext.applyConversationReference({ type: 'invoke' }, reference, true);

const testTraceMessage = {
type: 'trace',
name: 'TestTrace',
valueType: 'https://example.org/test/trace',
label: 'Test Trace'
};

class AdapterUnderTest extends BotFrameworkAdapter {
constructor(settings) {
super(settings);
Expand Down Expand Up @@ -551,6 +566,49 @@ describe(`BotFrameworkAdapter`, function () {
assert.strictEqual(res.statusCode, StatusCodes.OK);
});

it(`should remove trace activities from bufferedReplyActivities if request.channelId !== Channels.Emulator`, async () => {
const activity = JSON.parse(JSON.stringify(incomingMessage));
activity.type = ActivityTypes.Invoke;
activity.deliveryMode = DeliveryModes.ExpectReplies;
activity.channelId = Channels.Msteams;

const req = new MockRequest(activity);
const res = new MockResponse();
const adapter = new AdapterUnderTest();
await adapter.processActivity(req, res, async (context) => {
await context.sendActivity({ type: 'invokeResponse', text: 'message' });
await context.sendActivity(testTraceMessage);
});

const bufferedReplies = res.body && res.body.activities;
assert.strictEqual(bufferedReplies && bufferedReplies.length, 1);
assert.strictEqual(bufferedReplies[0].text, 'message');
assert.strictEqual(bufferedReplies[0].type, 'invokeResponse');
assert.strictEqual(res.statusCode, StatusCodes.OK);
});

it(`should keep trace activities from bufferedReplyActivities if request.channelId === Channels.Emulator`, async () => {
const activity = JSON.parse(JSON.stringify(incomingMessage));
activity.type = ActivityTypes.Invoke;
activity.deliveryMode = DeliveryModes.ExpectReplies;
activity.channelId = Channels.Emulator;

const req = new MockRequest(activity);
const res = new MockResponse();
const adapter = new AdapterUnderTest();
await adapter.processActivity(req, res, async (context) => {
await context.sendActivity({ type: 'invokeResponse', text: 'message' });
await context.sendActivity(testTraceMessage);
});

const bufferedReplies = res.body && res.body.activities;
assert.strictEqual(bufferedReplies && bufferedReplies.length, 2);
assert.strictEqual(bufferedReplies[0].text, 'message');
assert.strictEqual(bufferedReplies[0].type, 'invokeResponse');
assert.strictEqual(bufferedReplies[1].type, ActivityTypes.Trace);
assert.strictEqual(res.statusCode, StatusCodes.OK);
});

it(`processActivity() should not respect invokeResponses if the incoming request wasn't of type "invoke"`, async () => {
const req = new MockRequest(incomingMessage);
const res = new MockResponse();
Expand Down