Skip to content

Commit 68e1408

Browse files
committed
Fixes BadRequest message in Teams (#3966)
* Added check to ensure we don't send EoC when a skill is done and we are at the root bot. Updated tests to handle this case and removed duplicated test after refactoring. Fixed some typos and trace message names.
1 parent 84aa5a2 commit 68e1408

File tree

2 files changed

+113
-29
lines changed

2 files changed

+113
-29
lines changed

libraries/Microsoft.Bot.Builder.Dialogs/DialogManager.cs

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ public DialogManager(Dialog rootDialog = null, string dialogStateProperty = null
5656
public UserState UserState { get; set; }
5757

5858
/// <summary>
59-
/// Gets InitialTurnState collection to copy into the turnstate on every turn.
59+
/// Gets InitialTurnState collection to copy into the TurnState on every turn.
6060
/// </summary>
6161
/// <value>
6262
/// TurnState.
@@ -136,7 +136,7 @@ public async Task<DialogManagerResult> OnTurnAsync(ITurnContext context, Cancell
136136
context.TurnState.Set(pair.Key, pair.Value);
137137
}
138138

139-
// register DialogManager with turnstate.
139+
// register DialogManager with TurnState.
140140
context.TurnState.Set(this);
141141

142142
if (ConversationState == null)
@@ -185,13 +185,13 @@ public async Task<DialogManagerResult> OnTurnAsync(ITurnContext context, Cancell
185185
// Create DialogContext
186186
var dc = new DialogContext(Dialogs, context, dialogState);
187187

188-
// promote initial turnstate into dc.services for contextual services
188+
// promote initial TurnState into dc.services for contextual services
189189
foreach (var service in dc.Services)
190190
{
191191
dc.Services[service.Key] = service.Value;
192192
}
193193

194-
// map turnstate into root dialog context.services
194+
// map TurnState into root dialog context.services
195195
foreach (var service in context.TurnState)
196196
{
197197
dc.Services[service.Key] = service.Value;
@@ -295,7 +295,7 @@ private async Task<DialogTurnResult> HandleSkillOnTurnAsync(DialogContext dc, Ca
295295
var activeDialogContext = GetActiveDialogContext(dc);
296296

297297
var remoteCancelText = "Skill was canceled through an EndOfConversation activity from the parent.";
298-
await turnContext.TraceActivityAsync($"{typeof(Dialog).Name}.RunAsync()", label: $"{remoteCancelText}", cancellationToken: cancellationToken).ConfigureAwait(false);
298+
await turnContext.TraceActivityAsync($"{GetType().Name}.OnTurnAsync()", label: $"{remoteCancelText}", cancellationToken: cancellationToken).ConfigureAwait(false);
299299

300300
// Send cancellation message to the top dialog in the stack to ensure all the parents are canceled in the right order.
301301
return await activeDialogContext.CancelAllDialogsAsync(true, cancellationToken: cancellationToken).ConfigureAwait(false);
@@ -321,20 +321,23 @@ private async Task<DialogTurnResult> HandleSkillOnTurnAsync(DialogContext dc, Ca
321321
{
322322
// restart root dialog
323323
var startMessageText = $"Starting {_rootDialogId}.";
324-
await turnContext.TraceActivityAsync($"{typeof(Dialog).Name}.RunAsync()", label: $"{startMessageText}", cancellationToken: cancellationToken).ConfigureAwait(false);
324+
await turnContext.TraceActivityAsync($"{GetType().Name}.OnTurnAsync()", label: $"{startMessageText}", cancellationToken: cancellationToken).ConfigureAwait(false);
325325
turnResult = await dc.BeginDialogAsync(_rootDialogId, cancellationToken: cancellationToken).ConfigureAwait(false);
326326
}
327327

328328
await SendStateSnapshotTraceAsync(dc, "Skill State", cancellationToken).ConfigureAwait(false);
329329

330-
// Send end of conversation if it is completed or cancelled.
331-
if (turnResult.Status == DialogTurnStatus.Complete || turnResult.Status == DialogTurnStatus.Cancelled)
330+
if (ShouldSendEndOfConversationToParent(turnContext, turnResult))
332331
{
333332
var endMessageText = $"Dialog {_rootDialogId} has **completed**. Sending EndOfConversation.";
334-
await turnContext.TraceActivityAsync($"{typeof(Dialog).Name}.RunAsync()", label: $"{endMessageText}", value: turnResult.Result, cancellationToken: cancellationToken).ConfigureAwait(false);
333+
await turnContext.TraceActivityAsync($"{GetType().Name}.OnTurnAsync()", label: $"{endMessageText}", value: turnResult.Result, cancellationToken: cancellationToken).ConfigureAwait(false);
335334

336335
// Send End of conversation at the end.
337-
var activity = new Activity(ActivityTypes.EndOfConversation) { Value = turnResult.Result, Locale = turnContext.Activity.Locale };
336+
var activity = new Activity(ActivityTypes.EndOfConversation)
337+
{
338+
Value = turnResult.Result,
339+
Locale = turnContext.Activity.Locale
340+
};
338341
await turnContext.SendActivityAsync(activity, cancellationToken).ConfigureAwait(false);
339342
}
340343

@@ -368,5 +371,33 @@ private async Task<DialogTurnResult> HandleBotOnTurnAsync(DialogContext dc, Canc
368371

369372
return turnResult;
370373
}
374+
375+
/// <summary>
376+
/// Helper to determine if we should send an EndOfConversation to the parent or not.
377+
/// </summary>
378+
private bool ShouldSendEndOfConversationToParent(ITurnContext context, DialogTurnResult turnResult)
379+
{
380+
if (!(turnResult.Status == DialogTurnStatus.Complete || turnResult.Status == DialogTurnStatus.Cancelled))
381+
{
382+
// The dialog is still going, don't return EoC.
383+
return false;
384+
}
385+
386+
if (context.TurnState.Get<IIdentity>(BotAdapter.BotIdentityKey) is ClaimsIdentity claimIdentity && SkillValidation.IsSkillClaim(claimIdentity.Claims))
387+
{
388+
// EoC Activities returned by skills are bounced back to the bot by SkillHandler.
389+
// In those cases we will have a SkillConversationReference instance in state.
390+
var skillConversationReference = context.TurnState.Get<SkillConversationReference>(SkillHandler.SkillConversationReferenceKey);
391+
if (skillConversationReference != null)
392+
{
393+
// If the skillConversationReference.OAuthScope is for one of the supported channels, we are at the root and we should not send an EoC.
394+
return skillConversationReference.OAuthScope != AuthenticationConstants.ToChannelFromBotOAuthScope && skillConversationReference.OAuthScope != GovernmentAuthenticationConstants.ToChannelFromBotOAuthScope;
395+
}
396+
397+
return true;
398+
}
399+
400+
return false;
401+
}
371402
}
372403
}

tests/Microsoft.Bot.Builder.Dialogs.Tests/DialogManagerTests.cs

Lines changed: 72 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
using System;
66
using System.Collections.Generic;
7+
using System.Linq;
78
using System.Security.Claims;
89
using System.Threading;
910
using System.Threading.Tasks;
@@ -27,9 +28,38 @@ public class DialogManagerTests
2728
// An App ID for a skill bot.
2829
private readonly string _skillBotId = Guid.NewGuid().ToString();
2930

31+
// Captures an EndOfConversation if it was sent to help with assertions.
32+
private Activity _eocSent;
33+
3034
// Property to capture the DialogManager turn results and do assertions.
3135
private DialogManagerResult _dmTurnResult;
3236

37+
/// <summary>
38+
/// Enum to handle different skill test cases.
39+
/// </summary>
40+
public enum SkillFlowTestCase
41+
{
42+
/// <summary>
43+
/// DialogManager is executing on a root bot with no skills (typical standalone bot).
44+
/// </summary>
45+
RootBotOnly,
46+
47+
/// <summary>
48+
/// DialogManager is executing on a root bot handling replies from a skill.
49+
/// </summary>
50+
RootBotConsumingSkill,
51+
52+
/// <summary>
53+
/// DialogManager is executing in a skill that is called from a root and calling another skill.
54+
/// </summary>
55+
MiddleSkill,
56+
57+
/// <summary>
58+
/// DialogManager is executing in a skill that is called from a parent (a root or another skill) but doesn't call another skill.
59+
/// </summary>
60+
LeafSkill
61+
}
62+
3363
public TestContext TestContext { get; set; }
3464

3565
[TestMethod]
@@ -210,26 +240,35 @@ public async Task DialogManager_DialogSet()
210240
}
211241

212242
[TestMethod]
213-
public async Task SkillSendsEoCAndValuesAtDialogEnd()
243+
[DataRow(SkillFlowTestCase.RootBotOnly, false)]
244+
[DataRow(SkillFlowTestCase.RootBotConsumingSkill, false)]
245+
[DataRow(SkillFlowTestCase.MiddleSkill, true)]
246+
[DataRow(SkillFlowTestCase.LeafSkill, true)]
247+
public async Task HandlesBotAndSkillsTestCases(SkillFlowTestCase testCase, bool shouldSendEoc)
214248
{
215249
var firstConversationId = Guid.NewGuid().ToString();
216250
var storage = new MemoryStorage();
217251

218252
var adaptiveDialog = CreateTestDialog(property: "conversation.name");
219-
220-
await CreateFlow(adaptiveDialog, storage, firstConversationId, isSkillFlow: true, locale: "en-GB")
221-
.Send("hi")
253+
await CreateFlow(adaptiveDialog, storage, firstConversationId, testCase: testCase, locale: "en-GB").Send("Hi")
222254
.AssertReply("Hello, what is your name?")
223-
.Send("Carlos")
224-
.AssertReply("Hello Carlos, nice to meet you!")
225-
.AssertReply(activity =>
226-
{
227-
Assert.AreEqual(activity.Type, ActivityTypes.EndOfConversation);
228-
Assert.AreEqual(((Activity)activity).Value, "Carlos");
229-
Assert.AreEqual(((Activity)activity).Locale, "en-GB");
230-
})
255+
.Send("SomeName")
256+
.AssertReply("Hello SomeName, nice to meet you!")
231257
.StartTestAsync();
258+
232259
Assert.AreEqual(DialogTurnStatus.Complete, _dmTurnResult.TurnResult.Status);
260+
261+
if (shouldSendEoc)
262+
{
263+
Assert.IsNotNull(_eocSent, "Skills should send EndConversation to channel");
264+
Assert.AreEqual(ActivityTypes.EndOfConversation, _eocSent.Type);
265+
Assert.AreEqual("SomeName", _eocSent.Value);
266+
Assert.AreEqual("en-GB", _eocSent.Locale);
267+
}
268+
else
269+
{
270+
Assert.IsNull(_eocSent, "Root bot should not send EndConversation to channel");
271+
}
233272
}
234273

235274
[TestMethod]
@@ -242,7 +281,7 @@ public async Task SkillHandlesEoCFromParent()
242281

243282
var eocActivity = new Activity(ActivityTypes.EndOfConversation);
244283

245-
await CreateFlow(adaptiveDialog, storage, firstConversationId, isSkillFlow: true, isSkillResponse: false)
284+
await CreateFlow(adaptiveDialog, storage, firstConversationId, testCase: SkillFlowTestCase.LeafSkill)
246285
.Send("hi")
247286
.AssertReply("Hello, what is your name?")
248287
.Send(eocActivity)
@@ -261,7 +300,7 @@ public async Task SkillHandlesRepromptFromParent()
261300

262301
var repromptEvent = new Activity(ActivityTypes.Event) { Name = DialogEvents.RepromptDialog };
263302

264-
await CreateFlow(adaptiveDialog, storage, firstConversationId, isSkillFlow: true)
303+
await CreateFlow(adaptiveDialog, storage, firstConversationId, testCase: SkillFlowTestCase.LeafSkill)
265304
.Send("hi")
266305
.AssertReply("Hello, what is your name?")
267306
.Send(repromptEvent)
@@ -281,7 +320,7 @@ public async Task SkillShouldReturnEmptyOnRepromptWithNoDialog()
281320

282321
var repromptEvent = new Activity(ActivityTypes.Event) { Name = DialogEvents.RepromptDialog };
283322

284-
await CreateFlow(adaptiveDialog, storage, firstConversationId, isSkillFlow: true)
323+
await CreateFlow(adaptiveDialog, storage, firstConversationId, testCase: SkillFlowTestCase.LeafSkill)
285324
.Send(repromptEvent)
286325
.StartTestAsync();
287326

@@ -293,7 +332,7 @@ private Dialog CreateTestDialog(string property)
293332
return new AskForNameDialog(property.Replace(".", string.Empty), property);
294333
}
295334

296-
private TestFlow CreateFlow(Dialog dialog, IStorage storage, string conversationId, string dialogStateProperty = null, bool isSkillFlow = false, bool isSkillResponse = true, string locale = null)
335+
private TestFlow CreateFlow(Dialog dialog, IStorage storage, string conversationId, string dialogStateProperty = null, SkillFlowTestCase testCase = SkillFlowTestCase.RootBotOnly, string locale = null)
297336
{
298337
var convoState = new ConversationState(storage);
299338
var userState = new UserState(storage);
@@ -312,7 +351,7 @@ private TestFlow CreateFlow(Dialog dialog, IStorage storage, string conversation
312351
var dm = new DialogManager(dialog, dialogStateProperty: dialogStateProperty);
313352
return new TestFlow(adapter, async (turnContext, cancellationToken) =>
314353
{
315-
if (isSkillFlow)
354+
if (testCase != SkillFlowTestCase.RootBotOnly)
316355
{
317356
// Create a skill ClaimsIdentity and put it in TurnState so SkillValidation.IsSkillClaim() returns true.
318357
var claimsIdentity = new ClaimsIdentity();
@@ -321,14 +360,28 @@ private TestFlow CreateFlow(Dialog dialog, IStorage storage, string conversation
321360
claimsIdentity.AddClaim(new Claim(AuthenticationConstants.AuthorizedParty, _parentBotId));
322361
turnContext.TurnState.Add(BotAdapter.BotIdentityKey, claimsIdentity);
323362

324-
if (isSkillResponse)
363+
if (testCase == SkillFlowTestCase.RootBotConsumingSkill)
364+
{
365+
// Simulate the SkillConversationReference with a channel OAuthScope stored in TurnState.
366+
// This emulates a response coming to a root bot through SkillHandler.
367+
turnContext.TurnState.Add(SkillHandler.SkillConversationReferenceKey, new SkillConversationReference { OAuthScope = AuthenticationConstants.ToChannelFromBotOAuthScope });
368+
}
369+
370+
if (testCase == SkillFlowTestCase.MiddleSkill)
325371
{
326372
// Simulate the SkillConversationReference with a parent Bot ID stored in TurnState.
327373
// This emulates a response coming to a skill from another skill through SkillHandler.
328374
turnContext.TurnState.Add(SkillHandler.SkillConversationReferenceKey, new SkillConversationReference { OAuthScope = _parentBotId });
329375
}
330376
}
331377

378+
// Interceptor to capture the EoC activity if it was sent so we can assert it in the tests.
379+
turnContext.OnSendActivities(async (tc, activities, next) =>
380+
{
381+
_eocSent = activities.FirstOrDefault(activity => activity.Type == ActivityTypes.EndOfConversation);
382+
return await next().ConfigureAwait(false);
383+
});
384+
332385
// Capture the last DialogManager turn result for assertions.
333386
_dmTurnResult = await dm.OnTurnAsync(turnContext, cancellationToken: cancellationToken).ConfigureAwait(false);
334387
});
@@ -368,7 +421,7 @@ public override async Task<DialogTurnResult> BeginDialogAsync(DialogContext oute
368421
Text = "Hello, what is your name?"
369422
}
370423
},
371-
cancellationToken: cancellationToken)
424+
cancellationToken)
372425
.ConfigureAwait(false);
373426
}
374427

0 commit comments

Comments
 (0)