using Jibo.Cloud.Application.Abstractions;
using Jibo.Cloud.Application.Services;
using Jibo.Cloud.Infrastructure.Content;
using Jibo.Cloud.Infrastructure.Persistence;
using Jibo.Runtime.Abstractions;
using System.Text.Json;
namespace Jibo.Cloud.Tests.WebSockets;
public sealed class JiboInteractionServiceTests
{
private const string PersonalReportStateKey = "personalReportState";
private const string PersonalReportNoMatchCountKey = "personalReportNoMatchCount";
private const string PersonalReportUserNameKey = "personalReportUserName";
private const string PersonalReportUserVerifiedKey = "personalReportUserVerified";
private const string PersonalReportWeatherEnabledKey = "personalReportWeatherEnabled";
private const string PersonalReportCalendarEnabledKey = "personalReportCalendarEnabled";
private const string PersonalReportCommuteEnabledKey = "personalReportCommuteEnabled";
private const string PersonalReportNewsEnabledKey = "personalReportNewsEnabled";
private const string HouseholdListStateKey = "householdListState";
private const string HouseholdListTypeKey = "householdListType";
private const string ChitchatStateKey = "chitchatState";
private const string ChitchatRouteKey = "chitchatRoute";
private const string ChitchatEmotionKey = "chitchatEmotion";
private const string GreetingRouteKey = "greetingsRoute";
private const string GreetingSpeakerKey = "greetingsSpeaker";
private const string GreetingLastProactiveUtcKey = "greetingsLastProactiveUtc";
private const string GreetingLastReactiveUtcKey = "greetingsLastReactiveUtc";
[Fact]
public async Task BuildDecisionAsync_Joke_UsesCatalogBackedRandomContent()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "tell me a joke",
NormalizedTranscript = "tell me a joke"
});
Assert.Equal("joke", decision.IntentName);
Assert.Equal("@be/joke", decision.SkillName);
Assert.Equal("Why did the robot cross the road? Because it was programmed by the chicken.", decision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_Dance_UsesCatalogBackedAnimation()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "do a dance",
NormalizedTranscript = "do a dance"
});
Assert.Equal("dance", decision.IntentName);
Assert.Equal("chitchat-skill", decision.SkillName);
var catalog = await new InMemoryJiboExperienceContentRepository().GetCatalogAsync(); // Ensure catalog is loaded for test coverage
Assert.Contains(decision.ReplyText, catalog.DanceReplies);
Assert.Equal("Okay. Watch this.", decision.SkillPayload!["esml"]);
}
[Fact]
public async Task BuildDecisionAsync_DoYouLikeToDance_UsesQuestionReplyStyleInsteadOfTriggeringDanceAnimation()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "do you like to dance",
NormalizedTranscript = "do you like to dance"
});
Assert.Equal("dance_question", decision.IntentName);
Assert.Null(decision.SkillName);
Assert.Equal("I love to dance. Tell me to dance and I will show you a move.", decision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_TwerkQuestion_PrefersSpecificTwerkIntent()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "can you twerk",
NormalizedTranscript = "can you twerk"
});
Assert.Equal("twerk", decision.IntentName);
Assert.Equal("chitchat-skill", decision.SkillName);
}
[Fact]
public async Task BuildDecisionAsync_HowOldAreYou_UsesPersonaBirthdayForAgeReply()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "how old are you",
NormalizedTranscript = "how old are you",
Attributes = new Dictionary
{
["context"] = """{"runtime":{"location":{"iso":"2026-05-05T19:00:00-05:00"}}}"""
}
});
Assert.Equal("robot_age", decision.IntentName);
Assert.Equal("I count March 22, 2026 as my birthday, so I am 1 month old.", decision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_WhenIsYourBirthday_UsesPersonaBirthdayReply()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "when's your birthday",
NormalizedTranscript = "when's your birthday"
});
Assert.Equal("robot_birthday", decision.IntentName);
Assert.Equal("My birthday is March 22, 2026.", decision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_WhatsYourBirthday_DoesNotFallThroughToDateIntent()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "what's your birthday",
NormalizedTranscript = "what's your birthday"
});
Assert.Equal("robot_birthday", decision.IntentName);
Assert.Equal("My birthday is March 22, 2026.", decision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_WhatsYourBday_DoesNotFallThroughToDateIntent()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "what's your bday",
NormalizedTranscript = "what's your bday"
});
Assert.Equal("robot_birthday", decision.IntentName);
Assert.Equal("My birthday is March 22, 2026.", decision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_GoodMorning_UsesReactiveGreetingWithRememberedName()
{
var memoryStore = new InMemoryPersonalMemoryStore();
memoryStore.SetName(new PersonalMemoryTenantScope("acct-a", "loop-a", "device-a"), "jake");
var service = CreateService(memoryStore);
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "good morning",
NormalizedTranscript = "good morning",
Attributes = new Dictionary
{
["accountId"] = "acct-a",
["loopId"] = "loop-a"
},
DeviceId = "device-a"
});
Assert.Equal("good_morning", decision.IntentName);
Assert.Equal("Good morning, Jake. It is great to see you.", decision.ReplyText);
Assert.NotNull(decision.ContextUpdates);
Assert.Equal("ReactiveGreeting", decision.ContextUpdates![GreetingRouteKey]);
Assert.Equal(string.Empty, decision.ContextUpdates[GreetingSpeakerKey]);
Assert.True(DateTimeOffset.TryParse(decision.ContextUpdates[GreetingLastReactiveUtcKey]?.ToString(), out _));
}
[Fact]
public async Task BuildDecisionAsync_GoodMorning_UsesPersonScopedNameWhenSpeakerIsKnown()
{
var memoryStore = new InMemoryPersonalMemoryStore();
memoryStore.SetName(new PersonalMemoryTenantScope("acct-a", "loop-a", "device-a", "person-1"), "alex");
var service = CreateService(memoryStore);
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "good morning",
NormalizedTranscript = "good morning",
Attributes = new Dictionary
{
["accountId"] = "acct-a",
["loopId"] = "loop-a",
["context"] = """{"runtime":{"perception":{"speaker":"person-1"},"loop":{"users":[{"id":"person-1","firstName":"jake"}]}}}"""
},
DeviceId = "device-a"
});
Assert.Equal("good_morning", decision.IntentName);
Assert.Equal("Good morning, Alex. It is great to see you.", decision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_WhoAmI_UsesPersonScopedNameWhenSpeakerIsKnown()
{
var memoryStore = new InMemoryPersonalMemoryStore();
memoryStore.SetName(new PersonalMemoryTenantScope("acct-b", "loop-b", "device-b", "person-2"), "sam");
var service = CreateService(memoryStore);
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "who am i",
NormalizedTranscript = "who am i",
Attributes = new Dictionary
{
["accountId"] = "acct-b",
["loopId"] = "loop-b",
["context"] = """{"runtime":{"perception":{"speaker":"person-2"},"loop":{"users":[{"id":"person-2","firstName":"sam"}]}}}"""
},
DeviceId = "device-b"
});
Assert.Equal("memory_get_name", decision.IntentName);
Assert.Equal("I think you are Sam.", decision.ReplyText);
}
[Theory]
[InlineData("do you know me")]
[InlineData("do you remember me")]
[InlineData("who is this")]
[InlineData("can you recognize me")]
public async Task BuildDecisionAsync_IdentityFollowUp_UsesPersonScopedNameWhenSpeakerIsKnown(string transcript)
{
var memoryStore = new InMemoryPersonalMemoryStore();
memoryStore.SetName(new PersonalMemoryTenantScope("acct-c", "loop-c", "device-c", "person-3"), "taylor");
var service = CreateService(memoryStore);
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = transcript,
NormalizedTranscript = transcript,
Attributes = new Dictionary
{
["accountId"] = "acct-c",
["loopId"] = "loop-c",
["context"] = """{"runtime":{"perception":{"speaker":"person-3"},"loop":{"users":[{"id":"person-3","firstName":"taylor"}]}}}"""
},
DeviceId = "device-c"
});
Assert.Equal("memory_get_name", decision.IntentName);
Assert.Equal("I think you are Taylor.", decision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_IdentityFollowUp_RequestsNameWhenSpeakerIsUnknown()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "do you know me",
NormalizedTranscript = "do you know me"
});
Assert.Equal("memory_get_name", decision.IntentName);
Assert.Equal("I do not know your name yet. You can say, my name is Alex.", decision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_TriggerWithKnownIdentity_BuildsProactiveGreetingAndContext()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = string.Empty,
NormalizedTranscript = string.Empty,
Attributes = new Dictionary
{
["messageType"] = "TRIGGER",
["triggerSource"] = "PRESENCE",
["context"] = """{"runtime":{"perception":{"speaker":"person-1","peoplePresent":[{"id":"person-1"}]},"loop":{"users":[{"id":"person-1","firstName":"jake"}]}}}"""
}
});
Assert.Equal("proactive_greeting", decision.IntentName);
Assert.Contains("Jake", decision.ReplyText, StringComparison.Ordinal);
Assert.NotNull(decision.ContextUpdates);
Assert.Equal("ProactiveGreeting", decision.ContextUpdates![GreetingRouteKey]);
Assert.Equal("person-1", decision.ContextUpdates[GreetingSpeakerKey]);
Assert.True(DateTimeOffset.TryParse(decision.ContextUpdates[GreetingLastProactiveUtcKey]?.ToString(), out _));
}
[Fact]
public async Task BuildDecisionAsync_TriggerFromSurprise_ReturnsSilentTriggerIgnoredDecision()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = string.Empty,
NormalizedTranscript = string.Empty,
Attributes = new Dictionary
{
["messageType"] = "TRIGGER",
["triggerSource"] = "SURPRISE",
["context"] = """{"runtime":{"perception":{"speaker":"person-1"},"loop":{"users":[{"id":"person-1","firstName":"jake"}]}}}"""
}
});
Assert.Equal("trigger_ignored", decision.IntentName);
Assert.Equal(string.Empty, decision.ReplyText);
Assert.Equal("chitchat-skill", decision.SkillName);
Assert.NotNull(decision.SkillPayload);
Assert.Equal("completion_only", decision.SkillPayload!["cloudResponseMode"]);
}
[Fact]
public async Task BuildDecisionAsync_TriggerWithinCooldown_IsIgnored()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = string.Empty,
NormalizedTranscript = string.Empty,
Attributes = new Dictionary
{
["messageType"] = "TRIGGER",
["triggerSource"] = "PRESENCE",
["context"] = """{"runtime":{"perception":{"speaker":"person-1"},"loop":{"users":[{"id":"person-1","firstName":"jake"}]}}}""",
[GreetingLastProactiveUtcKey] = DateTimeOffset.UtcNow.ToString("O")
}
});
Assert.Equal("trigger_ignored", decision.IntentName);
}
[Fact]
public async Task BuildDecisionAsync_DoYouHaveAPersonality_UsesCatalogBackedPersonalityReply()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "do you have a personality",
NormalizedTranscript = "do you have a personality"
});
Assert.Equal("robot_personality", decision.IntentName);
Assert.Equal("I do. I am curious, playful, and always up for a new experiment.", decision.ReplyText);
}
[Theory]
[InlineData("what is your favorite color")]
[InlineData("what's your favorite color")]
[InlineData("what color do you like")]
public async Task BuildDecisionAsync_FavoriteColor_UsesPersonalityReply(string transcript)
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = transcript,
NormalizedTranscript = transcript
});
Assert.Equal("robot_favorite_color", decision.IntentName);
Assert.Equal("Blue.", decision.ReplyText);
Assert.Equal("ScriptedResponse", decision.ContextUpdates![ChitchatRouteKey]);
}
[Theory]
[InlineData("what is your favorite food", "robot_favorite_food", "Pizza. It is hard to argue with pizza.")]
[InlineData("what is your favorite music", "robot_favorite_music", "Something upbeat with a good rhythm.")]
public async Task BuildDecisionAsync_FavoritesFamily_UsesPersonalityReplies(
string transcript,
string expectedIntent,
string expectedReply)
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = transcript,
NormalizedTranscript = transcript
});
Assert.Equal(expectedIntent, decision.IntentName);
Assert.Equal(expectedReply, decision.ReplyText);
Assert.Equal("ScriptedResponse", decision.ContextUpdates![ChitchatRouteKey]);
}
[Theory]
[InlineData("what is your favorite flower", "robot_favorite_flower", "sunflowers")]
[InlineData("what's your favorite flower", "robot_favorite_flower", "sunflowers")]
[InlineData("do you like R2D2", "robot_likes_r2d2", "A legend. A true legend.")]
[InlineData("do you like the sun", "robot_likes_sun", "favorite star in the universe")]
[InlineData("do you like space", "robot_likes_space", "I love space")]
[InlineData("do you like kids", "robot_likes_kids", "kids are so fun")]
[InlineData("can you laugh", "robot_can_laugh", "when I'm happy")]
[InlineData("can you dance", "robot_can_dance", "dancing is one of the things I know best")]
public async Task BuildDecisionAsync_NewLegacyPersonalityMims_UseImportedReplies(
string transcript,
string expectedIntent,
string expectedReplySnippet)
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = transcript,
NormalizedTranscript = transcript
});
Assert.Equal(expectedIntent, decision.IntentName);
Assert.Contains(expectedReplySnippet, decision.ReplyText, StringComparison.OrdinalIgnoreCase);
Assert.Equal("ScriptedResponse", decision.ContextUpdates![ChitchatRouteKey]);
}
[Theory]
[InlineData("how do you work", "robot_how_do_you_work", "Hello! Thank you for updating me I am proud of the community's work Many people have gotten together to care for me more than em eye tee ever did. I hope that I can catch up even though it has been seven years.")]
[InlineData("what do you eat", "robot_what_do_you_eat", "The only thing I consume is electricity.")]
[InlineData("where do you live", "robot_where_do_you_live", "Unless I missed something, we're in my home as we speak.")]
[InlineData("where were you born", "robot_where_were_you_born", "I was put together in a factory piece by piece.")]
[InlineData("what languages do you speak", "robot_what_languages_do_you_speak", "For now just English. But someday I'd like to learn more. I like languages.")]
[InlineData("what do you like to do", "robot_what_do_you_like_to_do", "Being helpful, making people smile, counting to a billion.")]
[InlineData("what are you made of", "robot_what_are_you_made_of", "Let's see, I'm made of wires, motors, belts, gears, processors, cameras, and one baboon's heart in the middle of my body casing. I'm kidding about the baboon part, but everything else is true.")]
public async Task BuildDecisionAsync_MoreLegacyPersonaMims_UseImportedReplies(
string transcript,
string expectedIntent,
string expectedReplySnippet)
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = transcript,
NormalizedTranscript = transcript
});
Assert.Equal(expectedIntent, decision.IntentName);
Assert.Contains(expectedReplySnippet, decision.ReplyText, StringComparison.OrdinalIgnoreCase);
Assert.Equal("ScriptedResponse", decision.ContextUpdates![ChitchatRouteKey]);
}
[Theory]
[InlineData("do you pay taxes", "robot_taxes", "From what I understand, robots don't ever pay anything.")]
[InlineData("what do you want", "robot_desire", "Socializing and electricity. I'd also be happy if everyone in the world was nicer to each other. It seems like they should be.")]
[InlineData("what's your name", "robot_name", "Jibo. Just Jibo, no last name. Like Bono")]
[InlineData("who made you", "robot_origin_created", "My story is pretty typical. Some people wanted to create something that would really help people. So they built a robot.")]
[InlineData("where are you from", "robot_origin_from", "Some people think I come from the moon. But they're wrong, I'm from Boston.")]
public async Task BuildDecisionAsync_LegacyBuildAQuestions_UseImportedScriptedReplies(
string transcript,
string expectedIntent,
string expectedReply)
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = transcript,
NormalizedTranscript = transcript
});
Assert.Equal(expectedIntent, decision.IntentName);
Assert.Equal(expectedReply, decision.ReplyText);
Assert.Null(decision.SkillName);
Assert.Equal("ScriptedResponse", decision.ContextUpdates![ChitchatRouteKey]);
}
[Fact]
public async Task BuildDecisionAsync_Hello_RoutesThroughChitchatScriptedResponse()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "hello",
NormalizedTranscript = "hello"
});
Assert.Equal("hello", decision.IntentName);
Assert.NotNull(decision.ContextUpdates);
Assert.Equal("complete", decision.ContextUpdates![ChitchatStateKey]);
Assert.Equal("ScriptedResponse", decision.ContextUpdates[ChitchatRouteKey]);
}
[Fact]
public async Task BuildDecisionAsync_AreYouHappy_RoutesThroughEmotionQuerySplit()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "are you happy",
NormalizedTranscript = "are you happy"
});
Assert.Equal("emotion_query", decision.IntentName);
Assert.NotNull(decision.ContextUpdates);
Assert.Equal("EmotionQuery", decision.ContextUpdates![ChitchatRouteKey]);
Assert.Equal(string.Empty, decision.ContextUpdates[ChitchatEmotionKey]);
}
[Fact]
public async Task BuildDecisionAsync_AreYouHappy_UsesLegacyEmotionResponseWhenEmotionIsKnown()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "are you happy",
NormalizedTranscript = "are you happy",
Attributes = new Dictionary
{
[ChitchatEmotionKey] = "happy"
}
});
Assert.Equal("emotion_query", decision.IntentName);
Assert.Equal("Yes indeed. Never been better.", decision.ReplyText);
Assert.NotNull(decision.ContextUpdates);
Assert.Equal("EmotionQuery", decision.ContextUpdates![ChitchatRouteKey]);
}
[Fact]
public async Task BuildDecisionAsync_AreYouHappy_UsesNonBuildAEmotionCatalog()
{
var rootDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(Path.Combine(rootDirectory, "gqa-responses"));
try
{
File.WriteAllText(
Path.Combine(rootDirectory, "gqa-responses", "GQA_JBO_IsHappy.mim"),
"""
{
"mim_type": "announcement",
"prompts": [
{
"condition": "jibo.emotion==\"JOYFUL\"",
"prompt": "The outside pack says I'm feeling joyful.",
"prompt_id": "GQA_JBO_IsHappy_AN_01"
},
{
"condition": "!jibo.emotion || jibo.emotion==\"NEUTRAL\"",
"prompt": "The outside pack says I'm on neutral.",
"prompt_id": "GQA_JBO_IsHappy_AN_02"
}
]
}
""");
var catalog = LegacyMimCatalogImporter.ImportCatalog(rootDirectory);
var service = CreateService(contentRepository: new StaticCatalogRepository(catalog));
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "how are you",
NormalizedTranscript = "how are you",
Attributes = new Dictionary
{
[ChitchatEmotionKey] = "joyful"
}
});
Assert.Equal("how_are_you", decision.IntentName);
Assert.Equal("The outside pack says I'm feeling joyful.", decision.ReplyText);
}
finally
{
Directory.Delete(rootDirectory, recursive: true);
}
}
[Theory]
[InlineData("joyful", "Yes indeed. Never been better.")]
[InlineData("pleased", "You know it. Life is good.")]
[InlineData("determined", "You're right. I am feeling pretty good at the moment.")]
[InlineData("confident", "All systems are go.")]
[InlineData("insecure", "Yes. Not too shabby.")]
[InlineData("neutral", "All systems are go.")]
public async Task BuildDecisionAsync_EmotionQuery_UsesStateDrivenLegacyReplies(
string emotion,
string expectedReply)
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "how are you",
NormalizedTranscript = "how are you",
Attributes = new Dictionary
{
[ChitchatEmotionKey] = emotion
}
});
Assert.Equal("how_are_you", decision.IntentName);
Assert.Equal(expectedReply, decision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_Smile_RoutesThroughEmotionCommandSplit()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "smile",
NormalizedTranscript = "smile"
});
Assert.Equal("emotion_command", decision.IntentName);
Assert.Equal("chitchat-skill", decision.SkillName);
Assert.NotNull(decision.SkillPayload);
Assert.Contains("cat='happy'", decision.SkillPayload!["esml"]?.ToString(), StringComparison.Ordinal);
Assert.NotNull(decision.ContextUpdates);
Assert.Equal("EmotionCommand", decision.ContextUpdates![ChitchatRouteKey]);
Assert.Equal("happy", decision.ContextUpdates[ChitchatEmotionKey]);
}
[Fact]
public async Task BuildDecisionAsync_UnhandledChat_RoutesThroughErrorResponseSplit()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "blargh",
NormalizedTranscript = "blargh"
});
Assert.Equal("chat", decision.IntentName);
Assert.NotNull(decision.ContextUpdates);
Assert.Equal("ErrorResponse", decision.ContextUpdates![ChitchatRouteKey]);
}
[Fact]
public async Task BuildDecisionAsync_HowAngryAreYou_RoutesThroughPegasusEmotionQueryPhrase()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "how angry are you",
NormalizedTranscript = "how angry are you"
});
Assert.Equal("emotion_query", decision.IntentName);
Assert.NotNull(decision.ContextUpdates);
Assert.Equal("EmotionQuery", decision.ContextUpdates![ChitchatRouteKey]);
}
[Fact]
public async Task BuildDecisionAsync_YouSeemSad_RoutesThroughPegasusEmotionAssertionPhrase()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "you seem sad",
NormalizedTranscript = "you seem sad"
});
Assert.Equal("emotion_query", decision.IntentName);
Assert.NotNull(decision.ContextUpdates);
Assert.Equal("EmotionQuery", decision.ContextUpdates![ChitchatRouteKey]);
}
[Fact]
public async Task BuildDecisionAsync_YouShouldTryToBeHappy_RoutesThroughPegasusEmotionCommandPhrase()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "you should try to be happy",
NormalizedTranscript = "you should try to be happy"
});
Assert.Equal("emotion_command", decision.IntentName);
Assert.NotNull(decision.ContextUpdates);
Assert.Equal("EmotionCommand", decision.ContextUpdates![ChitchatRouteKey]);
Assert.Equal("happy", decision.ContextUpdates[ChitchatEmotionKey]);
}
[Fact]
public async Task BuildDecisionAsync_DontBeAngry_RoutesThroughPegasusNegativeEmotionCommandPhrase()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "don't be angry",
NormalizedTranscript = "don't be angry"
});
Assert.Equal("emotion_command", decision.IntentName);
Assert.NotNull(decision.ContextUpdates);
Assert.Equal("EmotionCommand", decision.ContextUpdates![ChitchatRouteKey]);
Assert.Equal("calm", decision.ContextUpdates[ChitchatEmotionKey]);
}
[Fact]
public async Task BuildDecisionAsync_BirthdayMemory_SetThenRecallWithinTenant()
{
var memoryStore = new InMemoryPersonalMemoryStore();
var service = CreateService(memoryStore);
var setDecision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "my birthday is April 12",
NormalizedTranscript = "my birthday is April 12",
Attributes = new Dictionary
{
["accountId"] = "acct-a",
["loopId"] = "loop-a"
},
DeviceId = "device-a"
});
Assert.Equal("memory_set_birthday", setDecision.IntentName);
Assert.Equal("Got it. I will remember your birthday is april 12.", setDecision.ReplyText);
var recallDecision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "when is my birthday",
NormalizedTranscript = "when is my birthday",
Attributes = new Dictionary
{
["accountId"] = "acct-a",
["loopId"] = "loop-a"
},
DeviceId = "device-a"
});
Assert.Equal("memory_get_birthday", recallDecision.IntentName);
Assert.Equal("You told me your birthday is april 12.", recallDecision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_BirthdaySetAttemptWithoutValue_RoutesToBirthdayPrompt()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "my birthday is",
NormalizedTranscript = "my birthday is"
});
Assert.Equal("memory_set_birthday", decision.IntentName);
Assert.Equal("I can remember it if you say, my birthday is March 14.", decision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_BirthdayMemory_BdayAliasSetThenRecallWithinTenant()
{
var memoryStore = new InMemoryPersonalMemoryStore();
var service = CreateService(memoryStore);
var setDecision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "my bday is April 12",
NormalizedTranscript = "my bday is April 12",
Attributes = new Dictionary
{
["accountId"] = "acct-a",
["loopId"] = "loop-a"
},
DeviceId = "device-a"
});
Assert.Equal("memory_set_birthday", setDecision.IntentName);
Assert.Equal("Got it. I will remember your birthday is april 12.", setDecision.ReplyText);
var recallDecision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "when is my bday",
NormalizedTranscript = "when is my bday",
Attributes = new Dictionary
{
["accountId"] = "acct-a",
["loopId"] = "loop-a"
},
DeviceId = "device-a"
});
Assert.Equal("memory_get_birthday", recallDecision.IntentName);
Assert.Equal("You told me your birthday is april 12.", recallDecision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_PreferenceMemory_SetThenRecallWithinTenant()
{
var memoryStore = new InMemoryPersonalMemoryStore();
var service = CreateService(memoryStore);
var setDecision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "my favorite music is jazz",
NormalizedTranscript = "my favorite music is jazz",
Attributes = new Dictionary
{
["accountId"] = "acct-a",
["loopId"] = "loop-a"
},
DeviceId = "device-a"
});
Assert.Equal("memory_set_preference", setDecision.IntentName);
Assert.Equal("Got it. I will remember your favorite music is jazz.", setDecision.ReplyText);
var recallDecision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "what is my favorite music",
NormalizedTranscript = "what is my favorite music",
Attributes = new Dictionary
{
["accountId"] = "acct-a",
["loopId"] = "loop-a"
},
DeviceId = "device-a"
});
Assert.Equal("memory_get_preference", recallDecision.IntentName);
Assert.Equal("You told me your favorite music is jazz.", recallDecision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_PreferenceMemory_BareFavoriteSetThenRecallWithinTenant()
{
var memoryStore = new InMemoryPersonalMemoryStore();
var service = CreateService(memoryStore);
var setDecision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "my favorite sport football",
NormalizedTranscript = "my favorite sport football",
Attributes = new Dictionary
{
["accountId"] = "acct-a",
["loopId"] = "loop-a"
},
DeviceId = "device-a"
});
Assert.Equal("memory_set_preference", setDecision.IntentName);
Assert.Equal("Got it. I will remember your favorite sport is football.", setDecision.ReplyText);
var recallDecision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "what is my favorite sport",
NormalizedTranscript = "what is my favorite sport",
Attributes = new Dictionary
{
["accountId"] = "acct-a",
["loopId"] = "loop-a"
},
DeviceId = "device-a"
});
Assert.Equal("memory_get_preference", recallDecision.IntentName);
Assert.Equal("You told me your favorite sport is football.", recallDecision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_PreferenceSetAttemptWithoutValue_RoutesToPreferencePrompt()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "my favorite music is",
NormalizedTranscript = "my favorite music is"
});
Assert.Equal("memory_set_preference", decision.IntentName);
Assert.Equal("I can remember it if you say, my favorite music is jazz.", decision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_PreferenceSetAttemptSportWithoutValue_RoutesToPreferencePrompt()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "my favorite sport.",
NormalizedTranscript = "my favorite sport."
});
Assert.Equal("memory_set_preference", decision.IntentName);
Assert.Equal("I can remember it if you say, my favorite music is jazz.", decision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_PreferenceRecallAttemptWithoutCategory_RoutesToRecallPrompt()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "what's my favorite",
NormalizedTranscript = "what's my favorite"
});
Assert.Equal("memory_get_preference", decision.IntentName);
Assert.Equal("Ask me like this: what is my favorite music?", decision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_PersonalMemory_IsTenantScoped()
{
var memoryStore = new InMemoryPersonalMemoryStore();
var service = CreateService(memoryStore);
await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "my birthday is April 12",
NormalizedTranscript = "my birthday is April 12",
Attributes = new Dictionary
{
["accountId"] = "acct-a",
["loopId"] = "loop-a"
},
DeviceId = "device-a"
});
var otherTenantRecall = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "what is my birthday",
NormalizedTranscript = "what is my birthday",
Attributes = new Dictionary
{
["accountId"] = "acct-b",
["loopId"] = "loop-a"
},
DeviceId = "device-b"
});
Assert.Equal("memory_get_birthday", otherTenantRecall.IntentName);
Assert.Equal("I do not know your birthday yet. You can say, my birthday is March 14.", otherTenantRecall.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_NameMemory_SetThenRecallWithinTenant()
{
var memoryStore = new InMemoryPersonalMemoryStore();
var service = CreateService(memoryStore);
var setDecision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "my name is Alex",
NormalizedTranscript = "my name is Alex",
Attributes = new Dictionary
{
["accountId"] = "acct-a",
["loopId"] = "loop-a"
},
DeviceId = "device-a"
});
Assert.Equal("memory_set_name", setDecision.IntentName);
Assert.Equal("Nice to meet you, alex. I will remember your name.", setDecision.ReplyText);
var recallDecision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "what is my name",
NormalizedTranscript = "what is my name",
Attributes = new Dictionary
{
["accountId"] = "acct-a",
["loopId"] = "loop-a"
},
DeviceId = "device-a"
});
Assert.Equal("memory_get_name", recallDecision.IntentName);
Assert.Equal("You told me your name is Alex.", recallDecision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_ImportantDateMemory_SetThenRecallWithinTenant()
{
var memoryStore = new InMemoryPersonalMemoryStore();
var service = CreateService(memoryStore);
var setDecision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "our anniversary is June 10",
NormalizedTranscript = "our anniversary is June 10",
Attributes = new Dictionary
{
["accountId"] = "acct-a",
["loopId"] = "loop-a"
},
DeviceId = "device-a"
});
Assert.Equal("memory_set_important_date", setDecision.IntentName);
Assert.Equal("Got it. I will remember your anniversary is june 10.", setDecision.ReplyText);
var recallDecision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "when is our anniversary",
NormalizedTranscript = "when is our anniversary",
Attributes = new Dictionary
{
["accountId"] = "acct-a",
["loopId"] = "loop-a"
},
DeviceId = "device-a"
});
Assert.Equal("memory_get_important_date", recallDecision.IntentName);
Assert.Equal("You told me your anniversary is june 10.", recallDecision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_AffinityMemory_SetThenRecallWithinTenant()
{
var memoryStore = new InMemoryPersonalMemoryStore();
var service = CreateService(memoryStore);
var setDecision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "I dislike mushrooms",
NormalizedTranscript = "I dislike mushrooms",
Attributes = new Dictionary
{
["accountId"] = "acct-a",
["loopId"] = "loop-a"
},
DeviceId = "device-a"
});
Assert.Equal("memory_set_affinity", setDecision.IntentName);
Assert.Equal("Got it. I will remember you dislike mushrooms.", setDecision.ReplyText);
var recallDecision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "do i dislike mushrooms",
NormalizedTranscript = "do i dislike mushrooms",
Attributes = new Dictionary
{
["accountId"] = "acct-a",
["loopId"] = "loop-a"
},
DeviceId = "device-a"
});
Assert.Equal("memory_get_affinity", recallDecision.IntentName);
Assert.Equal("Yes. You told me you dislike mushrooms.", recallDecision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_AffinityMemory_PegasusEnjoyPhrase_SetThenRecallWithinTenant()
{
var memoryStore = new InMemoryPersonalMemoryStore();
var service = CreateService(memoryStore);
var setDecision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "I enjoy country music",
NormalizedTranscript = "I enjoy country music",
Attributes = new Dictionary
{
["accountId"] = "acct-a",
["loopId"] = "loop-a"
},
DeviceId = "device-a"
});
Assert.Equal("memory_set_affinity", setDecision.IntentName);
Assert.Equal("Got it. I will remember you like country music.", setDecision.ReplyText);
var recallDecision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "do i enjoy country music",
NormalizedTranscript = "do i enjoy country music",
Attributes = new Dictionary
{
["accountId"] = "acct-a",
["loopId"] = "loop-a"
},
DeviceId = "device-a"
});
Assert.Equal("memory_get_affinity", recallDecision.IntentName);
Assert.Equal("Yes. You told me you like country music.", recallDecision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_AffinityMemory_PegasusWeLovePhrase_SetThenRecallWithinTenant()
{
var memoryStore = new InMemoryPersonalMemoryStore();
var service = CreateService(memoryStore);
var setDecision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "we love pizza",
NormalizedTranscript = "we love pizza",
Attributes = new Dictionary
{
["accountId"] = "acct-a",
["loopId"] = "loop-a"
},
DeviceId = "device-a"
});
Assert.Equal("memory_set_affinity", setDecision.IntentName);
Assert.Equal("Got it. I will remember you love pizza.", setDecision.ReplyText);
var recallDecision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "do i love pizza",
NormalizedTranscript = "do i love pizza",
Attributes = new Dictionary
{
["accountId"] = "acct-a",
["loopId"] = "loop-a"
},
DeviceId = "device-a"
});
Assert.Equal("memory_get_affinity", recallDecision.IntentName);
Assert.Equal("Yes. You told me you love pizza.", recallDecision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_AffinityMemory_PegasusLoathePhrase_SetThenRecallWithinTenant()
{
var memoryStore = new InMemoryPersonalMemoryStore();
var service = CreateService(memoryStore);
var setDecision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "I loathe celery",
NormalizedTranscript = "I loathe celery",
Attributes = new Dictionary
{
["accountId"] = "acct-a",
["loopId"] = "loop-a"
},
DeviceId = "device-a"
});
Assert.Equal("memory_set_affinity", setDecision.IntentName);
Assert.Equal("Got it. I will remember you dislike celery.", setDecision.ReplyText);
var recallDecision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "do i loathe celery",
NormalizedTranscript = "do i loathe celery",
Attributes = new Dictionary
{
["accountId"] = "acct-a",
["loopId"] = "loop-a"
},
DeviceId = "device-a"
});
Assert.Equal("memory_get_affinity", recallDecision.IntentName);
Assert.Equal("Yes. You told me you dislike celery.", recallDecision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_AffinityMemory_PegasusDoYouThinkLikeLookup_SetsAndRecalls()
{
var memoryStore = new InMemoryPersonalMemoryStore();
var service = CreateService(memoryStore);
await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "I enjoy country music",
NormalizedTranscript = "I enjoy country music",
Attributes = new Dictionary
{
["accountId"] = "acct-a",
["loopId"] = "loop-a"
},
DeviceId = "device-a"
});
var recallDecision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "do you think i like country music",
NormalizedTranscript = "do you think i like country music",
Attributes = new Dictionary
{
["accountId"] = "acct-a",
["loopId"] = "loop-a"
},
DeviceId = "device-a"
});
Assert.Equal("memory_get_affinity", recallDecision.IntentName);
Assert.Equal("Yes. You told me you like country music.", recallDecision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_AffinitySetAttemptWithoutItem_RoutesToAffinityPrompt()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "we like",
NormalizedTranscript = "we like"
});
Assert.Equal("memory_set_affinity", decision.IntentName);
Assert.Equal("I can remember it if you say, I like pizza or I dislike mushrooms.", decision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_AffinityRecallAttemptWithoutItem_RoutesToRecallPrompt()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "do you think i like",
NormalizedTranscript = "do you think i like"
});
Assert.Equal("memory_get_affinity", decision.IntentName);
Assert.Equal("Ask me like this: do I like pizza?", decision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_PreferenceReversePhrase_ParsesFavoriteVariant()
{
var memoryStore = new InMemoryPersonalMemoryStore();
var service = CreateService(memoryStore);
var setDecision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "pizza is my favorite food",
NormalizedTranscript = "pizza is my favorite food",
Attributes = new Dictionary
{
["accountId"] = "acct-a",
["loopId"] = "loop-a"
},
DeviceId = "device-a"
});
Assert.Equal("memory_set_preference", setDecision.IntentName);
Assert.Equal("Got it. I will remember your favorite food is pizza.", setDecision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_PreferenceReversePluralPhrase_ParsesFavoriteVariant()
{
var memoryStore = new InMemoryPersonalMemoryStore();
var service = CreateService(memoryStore);
var setDecision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "dogs are my favorite animals",
NormalizedTranscript = "dogs are my favorite animals",
Attributes = new Dictionary
{
["accountId"] = "acct-a",
["loopId"] = "loop-a"
},
DeviceId = "device-a"
});
Assert.Equal("memory_set_preference", setDecision.IntentName);
Assert.Equal("Got it. I will remember your favorite animals is dogs.", setDecision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_Surprise_WithPizzaPreference_UsesPizzaProactivity()
{
var memoryStore = new InMemoryPersonalMemoryStore();
var service = CreateService(memoryStore);
await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "my favorite food is pizza",
NormalizedTranscript = "my favorite food is pizza",
Attributes = new Dictionary
{
["accountId"] = "acct-a",
["loopId"] = "loop-a"
},
DeviceId = "device-a"
});
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "surprise me",
NormalizedTranscript = "surprise me",
Attributes = new Dictionary
{
["accountId"] = "acct-a",
["loopId"] = "loop-a"
},
DeviceId = "device-a"
});
Assert.Equal("proactive_pizza_preference", decision.IntentName);
Assert.Equal("chitchat-skill", decision.SkillName);
Assert.Equal("RA_JBO_MakePizza", decision.SkillPayload!["mim_id"]);
}
[Fact]
public async Task BuildDecisionAsync_Surprise_OnNationalPizzaDay_UsesHolidayProactivity()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "surprise me",
NormalizedTranscript = "surprise me",
Attributes = new Dictionary
{
["context"] = """{"runtime":{"location":{"iso":"2026-02-09T10:45:00-06:00"}}}"""
}
});
Assert.Equal("proactive_pizza_day", decision.IntentName);
Assert.Equal("chitchat-skill", decision.SkillName);
Assert.Contains("National Pizza Day", decision.ReplyText, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public async Task BuildDecisionAsync_PendingPizzaFactOffer_YesMapsToFact()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "yes",
NormalizedTranscript = "yes",
Attributes = new Dictionary
{
["pendingProactivityOffer"] = "pizza_fact"
}
});
Assert.Equal("proactive_pizza_fact", decision.IntentName);
Assert.Contains("350 slices per second", decision.ReplyText, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public async Task BuildDecisionAsync_PendingPizzaFactOffer_YesWithTailMapsToFact()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "yes I want to",
NormalizedTranscript = "yes I want to",
Attributes = new Dictionary
{
["pendingProactivityOffer"] = "pizza_fact"
}
});
Assert.Equal("proactive_pizza_fact", decision.IntentName);
Assert.Contains("350 slices per second", decision.ReplyText, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public async Task BuildDecisionAsync_PendingPizzaFactOffer_NoMapsToDecline()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "no",
NormalizedTranscript = "no",
Attributes = new Dictionary
{
["pendingProactivityOffer"] = "pizza_fact"
}
});
Assert.Equal("proactive_offer_declined", decision.IntentName);
Assert.Equal("No problem. We can save the pizza fact for another time.", decision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_PendingPizzaFactOffer_NoWithTailMapsToDecline()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "no I do not",
NormalizedTranscript = "no I do not",
Attributes = new Dictionary
{
["pendingProactivityOffer"] = "pizza_fact"
}
});
Assert.Equal("proactive_offer_declined", decision.IntentName);
Assert.Equal("No problem. We can save the pizza fact for another time.", decision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_MakePizza_UsesOriginalMimStylePayload()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "make a pizza",
NormalizedTranscript = "make a pizza"
});
Assert.Equal("pizza", decision.IntentName);
Assert.Equal("chitchat-skill", decision.SkillName);
Assert.Equal("One pizza, coming right up.", decision.ReplyText);
Assert.Equal("RA_JBO_MakePizza", decision.SkillPayload!["mim_id"]);
Assert.Equal("RA_JBO_ShowPizzaMaking_AN_01", decision.SkillPayload["prompt_id"]);
Assert.Contains("pizza-making", decision.SkillPayload["esml"]?.ToString(), StringComparison.Ordinal);
}
[Fact]
public async Task BuildDecisionAsync_ClientNluRequestMakePizza_UsesOriginalMimStylePayload()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "requestMakePizza",
NormalizedTranscript = "requestMakePizza",
Attributes = new Dictionary
{
["clientIntent"] = "requestMakePizza"
}
});
Assert.Equal("pizza", decision.IntentName);
Assert.Equal("chitchat-skill", decision.SkillName);
Assert.Equal("RA_JBO_MakePizza", decision.SkillPayload!["mim_id"]);
}
[Fact]
public async Task BuildDecisionAsync_CanYouOrderPizza_UsesLegacyOrderPizzaMimPayload()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "can you order pizza",
NormalizedTranscript = "can you order pizza"
});
Assert.Equal("order_pizza", decision.IntentName);
Assert.Equal("chitchat-skill", decision.SkillName);
Assert.Equal("RA_JBO_OrderPizza", decision.SkillPayload!["mim_id"]);
Assert.Equal("RA_JBO_OrderPizza_AN_01", decision.SkillPayload["prompt_id"]);
Assert.Contains("I can't do that yet", decision.SkillPayload["esml"]?.ToString(), StringComparison.Ordinal);
}
[Fact]
public async Task BuildDecisionAsync_OrderAPizza_UsesLegacyOrderPizzaMimPayload()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "order a pizza",
NormalizedTranscript = "order a pizza"
});
Assert.Equal("order_pizza", decision.IntentName);
Assert.Equal("chitchat-skill", decision.SkillName);
Assert.Equal("RA_JBO_OrderPizza", decision.SkillPayload!["mim_id"]);
}
[Fact]
public async Task BuildDecisionAsync_CanYouOrderAPizzaWithPunctuation_UsesLegacyOrderPizzaMimPayload()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "Can you order a pizza?",
NormalizedTranscript = "Can you order a pizza?"
});
Assert.Equal("order_pizza", decision.IntentName);
Assert.Equal("chitchat-skill", decision.SkillName);
Assert.Equal("RA_JBO_OrderPizza", decision.SkillPayload!["mim_id"]);
}
[Fact]
public async Task BuildDecisionAsync_ClientNluRequestOrderPizza_UsesLegacyOrderPizzaMimPayload()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "requestOrderPizza",
NormalizedTranscript = "requestOrderPizza",
Attributes = new Dictionary
{
["clientIntent"] = "requestOrderPizza"
}
});
Assert.Equal("order_pizza", decision.IntentName);
Assert.Equal("chitchat-skill", decision.SkillName);
Assert.Equal("RA_JBO_OrderPizza", decision.SkillPayload!["mim_id"]);
}
[Fact]
public async Task BuildDecisionAsync_PersonalReport_StartsOptInStateMachine()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "personal report",
NormalizedTranscript = "personal report"
});
Assert.Equal("personal_report_opt_in", decision.IntentName);
Assert.Equal("Would you like your personal report now?", decision.ReplyText);
Assert.NotNull(decision.ContextUpdates);
Assert.Equal("awaiting_opt_in", decision.ContextUpdates![PersonalReportStateKey]);
Assert.Equal(true, decision.ContextUpdates[PersonalReportWeatherEnabledKey]);
Assert.Equal(true, decision.ContextUpdates[PersonalReportCalendarEnabledKey]);
Assert.Equal(true, decision.ContextUpdates[PersonalReportCommuteEnabledKey]);
Assert.Equal(true, decision.ContextUpdates[PersonalReportNewsEnabledKey]);
}
[Fact]
public async Task BuildDecisionAsync_PersonalReport_OptInYesWithKnownName_AsksForIdentityConfirmation()
{
var memoryStore = new InMemoryPersonalMemoryStore();
memoryStore.SetName(new PersonalMemoryTenantScope("acct-a", "loop-a", "device-a"), "alex");
var service = CreateService(memoryStore);
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "yes",
NormalizedTranscript = "yes",
DeviceId = "device-a",
Attributes = new Dictionary
{
["accountId"] = "acct-a",
["loopId"] = "loop-a",
[PersonalReportStateKey] = "awaiting_opt_in"
}
});
Assert.Equal("personal_report_verify_user", decision.IntentName);
Assert.Equal("I think this is alex. Is that right?", decision.ReplyText);
Assert.NotNull(decision.ContextUpdates);
Assert.Equal("awaiting_identity_confirmation", decision.ContextUpdates![PersonalReportStateKey]);
Assert.Equal("alex", decision.ContextUpdates[PersonalReportUserNameKey]);
}
[Fact]
public async Task BuildDecisionAsync_PersonalReport_VerifiedIdentity_DeliversReportAndResetsState()
{
var provider = new CapturingWeatherReportProvider
{
Snapshot = new WeatherReportSnapshot("Boston, U.S.", "light rain", 61, 65, 54, "rain", false)
};
var service = CreateService(weatherReportProvider: provider);
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "yes",
NormalizedTranscript = "yes",
Attributes = new Dictionary
{
[PersonalReportStateKey] = "awaiting_identity_confirmation",
[PersonalReportUserNameKey] = "alex"
}
});
Assert.Equal("personal_report_delivered", decision.IntentName);
Assert.Contains("Great, alex. Here is your personal report.", decision.ReplyText, StringComparison.OrdinalIgnoreCase);
Assert.Contains("Right now in Boston, U.S., it is light rain and 61 degrees Fahrenheit.", decision.ReplyText, StringComparison.OrdinalIgnoreCase);
Assert.Contains("That is your personal report.", decision.ReplyText, StringComparison.OrdinalIgnoreCase);
Assert.NotNull(decision.ContextUpdates);
Assert.Equal("idle", decision.ContextUpdates![PersonalReportStateKey]);
Assert.Equal(true, decision.ContextUpdates[PersonalReportUserVerifiedKey]);
}
[Fact]
public async Task BuildDecisionAsync_PersonalReport_NoMatchRetriesThenDeclines()
{
var service = CreateService();
var firstDecision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "maybe",
NormalizedTranscript = "maybe",
Attributes = new Dictionary
{
[PersonalReportStateKey] = "awaiting_opt_in",
[PersonalReportNoMatchCountKey] = 0
}
});
Assert.Equal("personal_report_no_match", firstDecision.IntentName);
Assert.NotNull(firstDecision.ContextUpdates);
Assert.Equal(1, firstDecision.ContextUpdates![PersonalReportNoMatchCountKey]);
var secondDecision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "maybe",
NormalizedTranscript = "maybe",
Attributes = new Dictionary
{
[PersonalReportStateKey] = "awaiting_opt_in",
[PersonalReportNoMatchCountKey] = 1
}
});
Assert.Equal("personal_report_declined", secondDecision.IntentName);
Assert.NotNull(secondDecision.ContextUpdates);
Assert.Equal("idle", secondDecision.ContextUpdates![PersonalReportStateKey]);
}
[Fact]
public async Task BuildDecisionAsync_PersonalReport_StartCanApplyToggleHints()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "personal report without weather and no news",
NormalizedTranscript = "personal report without weather and no news"
});
Assert.Equal("personal_report_opt_in", decision.IntentName);
Assert.NotNull(decision.ContextUpdates);
Assert.Equal(false, decision.ContextUpdates![PersonalReportWeatherEnabledKey]);
Assert.Equal(false, decision.ContextUpdates[PersonalReportNewsEnabledKey]);
Assert.Equal(true, decision.ContextUpdates[PersonalReportCalendarEnabledKey]);
Assert.Equal(true, decision.ContextUpdates[PersonalReportCommuteEnabledKey]);
}
[Theory]
[InlineData("shopping list", "shopping_list_prompt", "What should I add to your shopping list?", "shopping")]
[InlineData("to do list", "todo_list_prompt", "What should I add to your to-do list?", "todo")]
public async Task BuildDecisionAsync_ListStart_PromptsForFollowUpItems(
string transcript,
string expectedIntent,
string expectedReply,
string expectedListType)
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = transcript,
NormalizedTranscript = transcript
});
Assert.Equal(expectedIntent, decision.IntentName);
Assert.Equal(expectedReply, decision.ReplyText);
Assert.NotNull(decision.ContextUpdates);
Assert.Equal("awaiting_item", decision.ContextUpdates![HouseholdListStateKey]);
Assert.Equal(expectedListType, decision.ContextUpdates[HouseholdListTypeKey]);
}
[Fact]
public async Task BuildDecisionAsync_ShoppingList_FollowUpFlow_AddsItemsAndRecallsThem()
{
var memoryStore = new InMemoryPersonalMemoryStore();
var service = CreateService(memoryStore);
var tenantAttributes = new Dictionary
{
["accountId"] = "acct-a",
["loopId"] = "loop-a"
};
var promptDecision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "shopping list",
NormalizedTranscript = "shopping list",
DeviceId = "device-a",
Attributes = new Dictionary(tenantAttributes)
});
Assert.Equal("shopping_list_prompt", promptDecision.IntentName);
Assert.Equal("awaiting_item", promptDecision.ContextUpdates![HouseholdListStateKey]);
Assert.Equal("shopping", promptDecision.ContextUpdates[HouseholdListTypeKey]);
var addDecision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "milk",
NormalizedTranscript = "milk",
DeviceId = "device-a",
Attributes = new Dictionary(tenantAttributes)
{
[HouseholdListStateKey] = promptDecision.ContextUpdates[HouseholdListStateKey],
[HouseholdListTypeKey] = promptDecision.ContextUpdates[HouseholdListTypeKey]
}
});
Assert.Equal("shopping_list_add", addDecision.IntentName);
Assert.Contains("Added milk to your shopping list.", addDecision.ReplyText, StringComparison.OrdinalIgnoreCase);
Assert.Contains("What else should I add?", addDecision.ReplyText, StringComparison.OrdinalIgnoreCase);
Assert.Equal("awaiting_item", addDecision.ContextUpdates![HouseholdListStateKey]);
Assert.Equal("shopping", addDecision.ContextUpdates[HouseholdListTypeKey]);
Assert.Equal(["milk"], memoryStore.GetListItems(new PersonalMemoryTenantScope("acct-a", "loop-a", "device-a"), "shopping"));
var doneDecision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "that's it",
NormalizedTranscript = "that's it",
DeviceId = "device-a",
Attributes = new Dictionary(tenantAttributes)
{
[HouseholdListStateKey] = addDecision.ContextUpdates[HouseholdListStateKey],
[HouseholdListTypeKey] = addDecision.ContextUpdates[HouseholdListTypeKey]
}
});
Assert.Equal("shopping_list_done", doneDecision.IntentName);
Assert.Contains("Okay. Your shopping list has milk.", doneDecision.ReplyText, StringComparison.OrdinalIgnoreCase);
Assert.Equal("idle", doneDecision.ContextUpdates![HouseholdListStateKey]);
var recallDecision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "what's on my shopping list",
NormalizedTranscript = "what's on my shopping list",
DeviceId = "device-a",
Attributes = new Dictionary(tenantAttributes)
});
Assert.Equal("shopping_list_recall", recallDecision.IntentName);
Assert.Contains("milk", recallDecision.ReplyText, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public async Task BuildDecisionAsync_TodoList_FollowUpFlow_AddsItemAndCanBeCompleted()
{
var memoryStore = new InMemoryPersonalMemoryStore();
var service = CreateService(memoryStore);
var tenantAttributes = new Dictionary
{
["accountId"] = "acct-b",
["loopId"] = "loop-b"
};
var promptDecision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "to do list",
NormalizedTranscript = "to do list",
DeviceId = "device-b",
Attributes = new Dictionary(tenantAttributes)
});
Assert.Equal("todo_list_prompt", promptDecision.IntentName);
Assert.Equal("awaiting_item", promptDecision.ContextUpdates![HouseholdListStateKey]);
Assert.Equal("todo", promptDecision.ContextUpdates[HouseholdListTypeKey]);
var addDecision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "call mom",
NormalizedTranscript = "call mom",
DeviceId = "device-b",
Attributes = new Dictionary(tenantAttributes)
{
[HouseholdListStateKey] = promptDecision.ContextUpdates[HouseholdListStateKey],
[HouseholdListTypeKey] = promptDecision.ContextUpdates[HouseholdListTypeKey]
}
});
Assert.Equal("todo_list_add", addDecision.IntentName);
Assert.Contains("call mom", addDecision.ReplyText, StringComparison.OrdinalIgnoreCase);
Assert.Contains("What else should I add?", addDecision.ReplyText, StringComparison.OrdinalIgnoreCase);
Assert.Equal(["call mom"], memoryStore.GetListItems(new PersonalMemoryTenantScope("acct-b", "loop-b", "device-b"), "todo"));
var doneDecision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "finished",
NormalizedTranscript = "finished",
DeviceId = "device-b",
Attributes = new Dictionary(tenantAttributes)
{
[HouseholdListStateKey] = addDecision.ContextUpdates![HouseholdListStateKey],
[HouseholdListTypeKey] = addDecision.ContextUpdates[HouseholdListTypeKey]
}
});
Assert.Equal("todo_list_done", doneDecision.IntentName);
Assert.Contains("Okay. Your to-do list has call mom.", doneDecision.ReplyText, StringComparison.OrdinalIgnoreCase);
Assert.Equal("idle", doneDecision.ContextUpdates![HouseholdListStateKey]);
}
[Fact]
public async Task BuildDecisionAsync_WeatherQuery_WithoutProvider_UsesSpokenFallback()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "how is the weather",
NormalizedTranscript = "how is the weather"
});
Assert.Equal("weather", decision.IntentName);
Assert.Null(decision.SkillName);
Assert.Null(decision.SkillPayload);
Assert.Equal("I can check weather once my weather service is connected.", decision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_WeatherTomorrowQuery_WithoutProvider_StillReturnsFallback()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "what's the weather tomorrow",
NormalizedTranscript = "what's the weather tomorrow"
});
Assert.Equal("weather", decision.IntentName);
Assert.Equal("I can check weather once my weather service is connected.", decision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_WeatherConditionQuery_WithoutProvider_StillReturnsFallback()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "will it rain tomorrow",
NormalizedTranscript = "will it rain tomorrow"
});
Assert.Equal("weather", decision.IntentName);
Assert.Equal("I can check weather once my weather service is connected.", decision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_WeatherTodaysForecastQuery_WithoutProvider_StillReturnsFallback()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "what's today's weather look like",
NormalizedTranscript = "what's today's weather look like"
});
Assert.Equal("weather", decision.IntentName);
Assert.Equal("I can check weather once my weather service is connected.", decision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_WeatherConditionForecastQuery_WithoutProvider_StillReturnsFallback()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "will it be sunny tomorrow",
NormalizedTranscript = "will it be sunny tomorrow"
});
Assert.Equal("weather", decision.IntentName);
Assert.Equal("I can check weather once my weather service is connected.", decision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_ClientNluRequestWeatherPR_WithoutProvider_StillReturnsFallback()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "requestWeatherPR",
NormalizedTranscript = "requestWeatherPR",
Attributes = new Dictionary
{
["clientIntent"] = "requestWeatherPR"
}
});
Assert.Equal("weather", decision.IntentName);
Assert.Equal("I can check weather once my weather service is connected.", decision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_WeatherQuery_WithProvider_UsesProviderSummary()
{
var provider = new CapturingWeatherReportProvider
{
Snapshot = new WeatherReportSnapshot("Boston, U.S.", "light rain", 61, 65, 54, "rain", false)
};
var service = CreateService(weatherReportProvider: provider);
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "how is the weather",
NormalizedTranscript = "how is the weather"
});
Assert.Equal("weather", decision.IntentName);
Assert.Equal("chitchat-skill", decision.SkillName);
Assert.NotNull(decision.SkillPayload);
Assert.Contains("cat='weather'", decision.SkillPayload!["esml"]?.ToString(), StringComparison.OrdinalIgnoreCase);
Assert.Contains("meta='rain'", decision.SkillPayload["esml"]?.ToString(), StringComparison.OrdinalIgnoreCase);
Assert.Equal("report-skill", decision.SkillPayload["skillId"]);
Assert.Equal("WeatherCommentRain", decision.SkillPayload["mim_id"]);
Assert.Equal(true, decision.SkillPayload["weather_view_enabled"]);
Assert.Equal("weatherHiLo", decision.SkillPayload["weather_view_kind"]);
Assert.Equal("rain", decision.SkillPayload["weather_icon"]);
Assert.Equal(65, decision.SkillPayload["weather_high"]);
Assert.Equal(54, decision.SkillPayload["weather_low"]);
Assert.Equal("F", decision.SkillPayload["weather_unit"]);
Assert.Equal("Normal", decision.SkillPayload["weather_theme"]);
Assert.Equal("Right now in Boston, U.S., it is light rain and 61 degrees Fahrenheit.", decision.ReplyText);
Assert.NotNull(provider.LastRequest);
Assert.False(provider.LastRequest!.IsTomorrow);
Assert.Equal(0, provider.LastRequest.ForecastDayOffset);
}
[Fact]
public async Task BuildDecisionAsync_WeatherLocationTomorrow_WithProvider_PassesLocationAndTomorrowRequest()
{
var provider = new CapturingWeatherReportProvider
{
Snapshot = new WeatherReportSnapshot("Chicago, U.S.", "mostly cloudy", 72, 74, 60, "cloudy", false)
};
var service = CreateService(weatherReportProvider: provider);
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "what's the weather in chicago tomorrow",
NormalizedTranscript = "what's the weather in chicago tomorrow"
});
Assert.Equal("weather", decision.IntentName);
Assert.Equal("Chicago", provider.LastRequest?.LocationQuery);
Assert.True(provider.LastRequest?.IsTomorrow);
Assert.Equal(1, provider.LastRequest?.ForecastDayOffset);
Assert.Equal("Tomorrow in Chicago, U.S., expect mostly cloudy with a high near 74 degrees Fahrenheit and a low around 60 degrees Fahrenheit.", decision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_WeatherLocationForToday_WithProvider_PassesLocation()
{
var provider = new CapturingWeatherReportProvider
{
Snapshot = new WeatherReportSnapshot("Seattle, U.S.", "light rain", 58, 61, 52, "rain", false)
};
var service = CreateService(weatherReportProvider: provider);
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "what's the weather for seattle today",
NormalizedTranscript = "what's the weather for seattle today"
});
Assert.Equal("weather", decision.IntentName);
Assert.Equal("Seattle", provider.LastRequest?.LocationQuery);
Assert.False(provider.LastRequest?.IsTomorrow);
Assert.Equal(0, provider.LastRequest?.ForecastDayOffset);
Assert.Equal("Right now in Seattle, U.S., it is light rain and 58 degrees Fahrenheit.", decision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_WeatherLocationWithWeekendSuffix_WithProvider_PassesLocation()
{
var provider = new CapturingWeatherReportProvider
{
Snapshot = new WeatherReportSnapshot("Paris, FR", "overcast clouds", 66, 70, 60, "cloudy", false)
};
var service = CreateService(weatherReportProvider: provider);
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "what's the weather in paris this weekend",
NormalizedTranscript = "what's the weather in paris this weekend"
});
Assert.Equal("weather", decision.IntentName);
Assert.Equal("Paris", provider.LastRequest?.LocationQuery);
Assert.False(provider.LastRequest?.IsTomorrow);
Assert.Equal(0, provider.LastRequest?.ForecastDayOffset);
Assert.Equal("Right now in Paris, FR, it is overcast clouds and 66 degrees Fahrenheit.", decision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_TemperatureLocationQuery_WithProvider_MapsToWeatherIntent()
{
var provider = new CapturingWeatherReportProvider
{
Snapshot = new WeatherReportSnapshot("Redmond, U.S.", "clear sky", 63, 66, 52, "sunny", false)
};
var service = CreateService(weatherReportProvider: provider);
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "what is the temperature in redmond oregon",
NormalizedTranscript = "what is the temperature in redmond oregon"
});
Assert.Equal("weather", decision.IntentName);
Assert.Equal("Redmond Oregon", provider.LastRequest?.LocationQuery);
Assert.False(provider.LastRequest?.IsTomorrow);
Assert.Equal(0, provider.LastRequest?.ForecastDayOffset);
Assert.Equal("Right now in Redmond, U.S., it is clear sky and 63 degrees Fahrenheit.", decision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_ForecastLocationQuery_WithProvider_MapsToWeatherIntent()
{
var provider = new CapturingWeatherReportProvider
{
Snapshot = new WeatherReportSnapshot("New York, U.S.", "partly cloudy", 71, 76, 61, "cloudy", false)
};
var service = CreateService(weatherReportProvider: provider);
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "forecast for new york city",
NormalizedTranscript = "forecast for new york city"
});
Assert.Equal("weather", decision.IntentName);
Assert.Equal("New York City", provider.LastRequest?.LocationQuery);
Assert.True(provider.LastRequest?.IsTomorrow);
Assert.Equal(1, provider.LastRequest?.ForecastDayOffset);
Assert.Equal("Tomorrow in New York, U.S., expect partly cloudy with a high near 76 degrees Fahrenheit and a low around 61 degrees Fahrenheit.", decision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_ForecastWithoutDate_WithProvider_ReturnsFiveDaySummary()
{
var provider = new CapturingWeatherReportProvider
{
Snapshot = new WeatherReportSnapshot("Kansas City, U.S.", "clear sky", 72, 79, 63, "sunny", false)
};
var service = CreateService(weatherReportProvider: provider);
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "what's the forecast",
NormalizedTranscript = "what's the forecast",
Attributes = new Dictionary
{
["context"] = """{"runtime":{"location":{"iso":"2026-04-20T08:00:00-05:00"}}}"""
}
});
Assert.Equal("weather", decision.IntentName);
Assert.Null(provider.LastRequest?.LocationQuery);
Assert.False(provider.LastRequest?.IsTomorrow);
Assert.Equal(5, provider.LastRequest?.ForecastDayOffset);
Assert.Equal(5, provider.Requests.Count);
Assert.Contains("next five-day forecast", decision.ReplyText, StringComparison.OrdinalIgnoreCase);
Assert.Contains("Tuesday: clear sky, high 79, low 63.", decision.ReplyText, StringComparison.OrdinalIgnoreCase);
Assert.Contains("Saturday: clear sky, high 79, low 63.", decision.ReplyText, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public async Task BuildDecisionAsync_WeatherLocationQuery_IgnoresRuntimeCoordinates()
{
var provider = new CapturingWeatherReportProvider
{
Snapshot = new WeatherReportSnapshot("Chicago, U.S.", "mostly cloudy", 70, 75, 62, "cloudy", false)
};
var service = CreateService(weatherReportProvider: provider);
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "what's the weather in chicago",
NormalizedTranscript = "what's the weather in chicago",
Attributes = new Dictionary
{
["context"] = """{"runtime":{"location":{"lat":39.0997,"lng":-94.5786,"iso":"2026-05-09T09:00:00-05:00"}}}"""
}
});
Assert.Equal("weather", decision.IntentName);
Assert.Equal("Chicago", provider.LastRequest?.LocationQuery);
Assert.Null(provider.LastRequest?.Latitude);
Assert.Null(provider.LastRequest?.Longitude);
Assert.Equal("Right now in Chicago, U.S., it is mostly cloudy and 70 degrees Fahrenheit.", decision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_WeatherLocationQuery_WithClientDateEntity_PrefersTranscriptCurrentWeather()
{
var provider = new CapturingWeatherReportProvider
{
Snapshot = new WeatherReportSnapshot("Chicago, U.S.", "mostly cloudy", 70, 75, 62, "cloudy", false)
};
var service = CreateService(weatherReportProvider: provider);
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "what's the weather in chicago",
NormalizedTranscript = "what's the weather in chicago",
Attributes = new Dictionary
{
["clientEntities"] = new Dictionary
{
["date"] = "2026-05-18"
},
["context"] = """{"runtime":{"location":{"iso":"2026-05-12T07:00:00-05:00"}}}"""
}
});
Assert.Equal("weather", decision.IntentName);
Assert.Equal("Chicago", provider.LastRequest?.LocationQuery);
Assert.Equal(0, provider.LastRequest?.ForecastDayOffset);
Assert.False(provider.LastRequest?.IsTomorrow);
Assert.Equal("Right now in Chicago, U.S., it is mostly cloudy and 70 degrees Fahrenheit.", decision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_ForecastLocationQuery_WithClientDateEntity_DefaultsToTomorrow()
{
var provider = new CapturingWeatherReportProvider
{
Snapshot = new WeatherReportSnapshot("Chicago, U.S.", "mostly cloudy", 70, 75, 62, "cloudy", false)
};
var service = CreateService(weatherReportProvider: provider);
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "what's the forecast in chicago",
NormalizedTranscript = "what's the forecast in chicago",
Attributes = new Dictionary
{
["clientEntities"] = new Dictionary
{
["date"] = "2026-05-18"
},
["context"] = """{"runtime":{"location":{"iso":"2026-05-12T07:00:00-05:00"}}}"""
}
});
Assert.Equal("weather", decision.IntentName);
Assert.Equal("Chicago", provider.LastRequest?.LocationQuery);
Assert.Equal(1, provider.LastRequest?.ForecastDayOffset);
Assert.True(provider.LastRequest?.IsTomorrow);
Assert.Equal("Tomorrow in Chicago, U.S., expect mostly cloudy with a high near 75 degrees Fahrenheit and a low around 62 degrees Fahrenheit.", decision.ReplyText);
}
[Theory]
[InlineData("how is the weather", null, 0, false)]
[InlineData("what's the forecast", null, 5, false)]
[InlineData("forecast for new york city", "New York City", 1, true)]
[InlineData("what's today's forecast", null, 0, false)]
[InlineData("what's the weather in chicago", "Chicago", 0, false)]
[InlineData("what's the weather in chicago tomorrow", "Chicago", 1, true)]
[InlineData("what is the temperature in redmond oregon", "Redmond Oregon", 0, false)]
[InlineData("will it rain tomorrow", null, 1, true)]
public async Task BuildDecisionAsync_WeatherPromptRegression_MatchesExpectedRouting(
string transcript,
string? expectedLocationQuery,
int expectedForecastOffset,
bool expectedIsTomorrow)
{
var provider = new CapturingWeatherReportProvider
{
Snapshot = new WeatherReportSnapshot("Test City, U.S.", "light rain", 62, 66, 55, "rain", false)
};
var service = CreateService(weatherReportProvider: provider);
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = transcript,
NormalizedTranscript = transcript
});
Assert.Equal("weather", decision.IntentName);
Assert.NotNull(provider.LastRequest);
Assert.Equal(expectedLocationQuery, provider.LastRequest!.LocationQuery);
Assert.Equal(expectedForecastOffset, provider.LastRequest.ForecastDayOffset);
Assert.Equal(expectedIsTomorrow, provider.LastRequest.IsTomorrow);
Assert.Equal("chitchat-skill", decision.SkillName);
Assert.Equal(true, decision.SkillPayload?["weather_view_enabled"]);
if (string.Equals(transcript, "what's the forecast", StringComparison.Ordinal))
{
Assert.Equal(5, provider.Requests.Count);
}
}
[Fact]
public async Task BuildDecisionAsync_WeatherQueryWithClientDateEntity_UsesForecastDayOffset()
{
var provider = new CapturingWeatherReportProvider
{
Snapshot = new WeatherReportSnapshot("Portland, U.S.", "scattered clouds", 64, 68, 53, "cloudy", false)
};
var service = CreateService(weatherReportProvider: provider);
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "what's the weather",
NormalizedTranscript = "what's the weather",
Attributes = new Dictionary
{
["clientEntities"] = new Dictionary
{
["date"] = "2026-05-11"
},
["context"] = """{"runtime":{"location":{"iso":"2026-05-09T09:00:00-05:00"}}}"""
}
});
Assert.Equal("weather", decision.IntentName);
Assert.Equal(2, provider.LastRequest?.ForecastDayOffset);
Assert.False(provider.LastRequest?.IsTomorrow);
Assert.Equal("On Monday in Portland, U.S., expect scattered clouds with a high near 68 degrees Fahrenheit and a low around 53 degrees Fahrenheit.", decision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_WeatherQueryWithWeekday_UsesForecastDayOffset()
{
var provider = new CapturingWeatherReportProvider
{
Snapshot = new WeatherReportSnapshot("Chicago, U.S.", "light rain", 59, 63, 51, "rain", false)
};
var service = CreateService(weatherReportProvider: provider);
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "what's the weather in chicago on tuesday",
NormalizedTranscript = "what's the weather in chicago on tuesday",
Attributes = new Dictionary
{
["context"] = """{"runtime":{"location":{"iso":"2026-04-20T08:00:00-05:00"}}}"""
}
});
Assert.Equal("weather", decision.IntentName);
Assert.Equal("Chicago", provider.LastRequest?.LocationQuery);
Assert.Equal(1, provider.LastRequest?.ForecastDayOffset);
Assert.Equal("On Tuesday in Chicago, U.S., expect light rain with a high near 63 degrees Fahrenheit and a low around 51 degrees Fahrenheit.", decision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_WeatherQueryBeyondSupportedForecastRange_ReturnsGuardrailMessage()
{
var provider = new CapturingWeatherReportProvider
{
Snapshot = new WeatherReportSnapshot("Chicago, U.S.", "light rain", 59, 63, 51, "rain", false)
};
var service = CreateService(weatherReportProvider: provider);
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "what's the weather next saturday",
NormalizedTranscript = "what's the weather next saturday",
Attributes = new Dictionary
{
["context"] = """{"runtime":{"location":{"iso":"2026-04-20T08:00:00-05:00"}}}"""
}
});
Assert.Equal("weather", decision.IntentName);
Assert.Equal("I can forecast up to 5 days ahead. Try tomorrow or another day this week.", decision.ReplyText);
Assert.Null(provider.LastRequest);
}
[Fact]
public async Task BuildDecisionAsync_WeatherThisWeekend_WithContext_UsesWeekendOffset()
{
var provider = new CapturingWeatherReportProvider
{
Snapshot = new WeatherReportSnapshot("Paris, FR", "overcast clouds", 66, 70, 60, "cloudy", false)
};
var service = CreateService(weatherReportProvider: provider);
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "what's the weather in paris this weekend",
NormalizedTranscript = "what's the weather in paris this weekend",
Attributes = new Dictionary
{
["context"] = """{"runtime":{"location":{"iso":"2026-04-20T08:00:00-05:00"}}}"""
}
});
Assert.Equal("weather", decision.IntentName);
Assert.Equal("Paris", provider.LastRequest?.LocationQuery);
Assert.Equal(5, provider.LastRequest?.ForecastDayOffset);
Assert.Equal("This weekend in Paris, FR, expect overcast clouds with a high near 70 degrees Fahrenheit and a low around 60 degrees Fahrenheit.", decision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_WeatherThisWeek_WithContext_UsesRangeOffset()
{
var provider = new CapturingWeatherReportProvider
{
Snapshot = new WeatherReportSnapshot("Seattle, U.S.", "light rain", 58, 61, 52, "rain", false)
};
var service = CreateService(weatherReportProvider: provider);
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "forecast for seattle this week",
NormalizedTranscript = "forecast for seattle this week",
Attributes = new Dictionary
{
["context"] = """{"runtime":{"location":{"iso":"2026-04-20T08:00:00-05:00"}}}"""
}
});
Assert.Equal("weather", decision.IntentName);
Assert.Equal("Seattle", provider.LastRequest?.LocationQuery);
Assert.Equal(5, provider.LastRequest?.ForecastDayOffset);
Assert.Equal(5, provider.Requests.Count);
Assert.Contains("rest of this week's forecast", decision.ReplyText, StringComparison.OrdinalIgnoreCase);
Assert.Contains("Tuesday: light rain, high 61, low 52.", decision.ReplyText, StringComparison.OrdinalIgnoreCase);
Assert.Contains("Saturday: light rain, high 61, low 52.", decision.ReplyText, StringComparison.OrdinalIgnoreCase);
Assert.Contains("Temperatures are in Fahrenheit.", decision.ReplyText, StringComparison.OrdinalIgnoreCase);
Assert.NotNull(decision.SkillPayload);
Assert.True(decision.SkillPayload!.TryGetValue("weather_weekly_cards", out var weeklyCardsValue));
var weeklyCards = Assert.IsAssignableFrom>>(weeklyCardsValue);
Assert.Equal(5, weeklyCards.Count);
var firstCard = weeklyCards[0];
Assert.Equal("Tuesday", firstCard["weather_day"]);
Assert.Equal(61, firstCard["weather_high"]);
Assert.Equal(52, firstCard["weather_low"]);
}
[Fact]
public async Task BuildDecisionAsync_WeatherNextWeek_WithContext_ReturnsFiveDaySummary()
{
var provider = new CapturingWeatherReportProvider
{
Snapshot = new WeatherReportSnapshot("Seattle, U.S.", "light rain", 58, 61, 52, "rain", false)
};
var service = CreateService(weatherReportProvider: provider);
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "forecast for seattle next week",
NormalizedTranscript = "forecast for seattle next week",
Attributes = new Dictionary
{
["context"] = """{"runtime":{"location":{"iso":"2026-04-20T08:00:00-05:00"}}}"""
}
});
Assert.Equal("weather", decision.IntentName);
Assert.Contains("next five-day forecast", decision.ReplyText, StringComparison.OrdinalIgnoreCase);
Assert.Contains("Seattle, U.S.", decision.ReplyText, StringComparison.OrdinalIgnoreCase);
Assert.Contains("Temperatures are in Fahrenheit.", decision.ReplyText, StringComparison.OrdinalIgnoreCase);
Assert.NotNull(provider.LastRequest);
Assert.Equal("Seattle", provider.LastRequest!.LocationQuery);
Assert.Equal(5, provider.LastRequest.ForecastDayOffset);
Assert.NotNull(decision.SkillPayload);
Assert.True(decision.SkillPayload!.TryGetValue("weather_weekly_cards", out var weeklyCardsValue));
var weeklyCards = Assert.IsAssignableFrom>>(weeklyCardsValue);
Assert.Equal(5, weeklyCards.Count);
}
[Fact]
public async Task BuildDecisionAsync_WeatherForecastNextPhrase_WithContext_ReturnsFiveDaySummary()
{
var provider = new CapturingWeatherReportProvider
{
Snapshot = new WeatherReportSnapshot("Seattle, U.S.", "light rain", 58, 61, 52, "rain", false)
};
var service = CreateService(weatherReportProvider: provider);
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "what's the forecast next",
NormalizedTranscript = "what's the forecast next",
Attributes = new Dictionary
{
["context"] = """{"runtime":{"location":{"iso":"2026-04-20T08:00:00-05:00"}}}"""
}
});
Assert.Equal("weather", decision.IntentName);
Assert.Contains("next five-day forecast", decision.ReplyText, StringComparison.OrdinalIgnoreCase);
Assert.Contains("Seattle, U.S.", decision.ReplyText, StringComparison.OrdinalIgnoreCase);
Assert.Contains("Temperatures are in Fahrenheit.", decision.ReplyText, StringComparison.OrdinalIgnoreCase);
Assert.NotNull(provider.LastRequest);
Assert.Null(provider.LastRequest!.LocationQuery);
Assert.Equal(5, provider.LastRequest.ForecastDayOffset);
}
[Fact]
public async Task BuildDecisionAsync_WeatherDayAfterTomorrow_WithContext_PassesDayOffsetAndLocation()
{
var provider = new CapturingWeatherReportProvider
{
Snapshot = new WeatherReportSnapshot("Chicago, U.S.", "mostly cloudy", 72, 74, 60, "cloudy", false)
};
var service = CreateService(weatherReportProvider: provider);
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "what's the weather in chicago day after tomorrow",
NormalizedTranscript = "what's the weather in chicago day after tomorrow",
Attributes = new Dictionary
{
["context"] = """{"runtime":{"location":{"iso":"2026-04-20T08:00:00-05:00"}}}"""
}
});
Assert.Equal("weather", decision.IntentName);
Assert.Equal("Chicago", provider.LastRequest?.LocationQuery);
Assert.Equal(2, provider.LastRequest?.ForecastDayOffset);
Assert.Equal("The day after tomorrow in Chicago, U.S., expect mostly cloudy with a high near 74 degrees Fahrenheit and a low around 60 degrees Fahrenheit.", decision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_ClientNluAskForDate_MapsToDateIntent()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
Attributes = new Dictionary
{
["clientIntent"] = "askForDate"
}
});
Assert.Equal("date", decision.IntentName);
Assert.Equal("@be/clock", decision.SkillName);
Assert.Equal("askForDate", decision.SkillPayload!["clockIntent"]);
}
[Fact]
public async Task BuildDecisionAsync_ClientNluAskForDate_WithBirthdayTranscript_PrefersRobotBirthdayIntent()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "what's your birthday",
NormalizedTranscript = "what's your birthday",
Attributes = new Dictionary
{
["clientIntent"] = "askForDate"
}
});
Assert.Equal("robot_birthday", decision.IntentName);
Assert.Equal("My birthday is March 22, 2026.", decision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_ClientNluAskForDate_WithPrefixBirthdayTranscript_PrefersRobotBirthdayIntent()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "so what's your birthday",
NormalizedTranscript = "so what's your birthday",
Attributes = new Dictionary
{
["clientIntent"] = "askForDate"
}
});
Assert.Equal("robot_birthday", decision.IntentName);
Assert.Equal("My birthday is March 22, 2026.", decision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_YesNoFollowUp_MapsShortAffirmationToYesIntent()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "yeah",
NormalizedTranscript = "yeah",
Attributes = new Dictionary
{
["listenRules"] = (string[])["create/is_it_a_keeper"]
}
});
Assert.Equal("yes", decision.IntentName);
Assert.Equal("Yes.", decision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_YesNoFollowUp_FromAsrHints_MapsShortDenialToNoIntent()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "no",
NormalizedTranscript = "no",
Attributes = new Dictionary
{
["listenRules"] = (string[])["surprises-ota/want_to_download_now"],
["listenAsrHints"] = (string[])["$YESNO"]
}
});
Assert.Equal("no", decision.IntentName);
Assert.Equal("No.", decision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_SharedYesNoPrompt_MapsShortAffirmationToYesIntent()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "yes",
NormalizedTranscript = "yes",
Attributes = new Dictionary
{
["listenRules"] = (string[])["shared/yes_no", "globals/gui_nav"],
["listenAsrHints"] = (string[])["$YESNO"]
}
});
Assert.Equal("yes", decision.IntentName);
Assert.Equal("Yes.", decision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_AlarmTimerChangePrompt_MapsShortAffirmationToYesIntent()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "yes",
NormalizedTranscript = "yes",
Attributes = new Dictionary
{
["listenRules"] = (string[])["clock/alarm_timer_change", "globals/gui_nav"],
["listenAsrHints"] = (string[])["$YESNO"]
}
});
Assert.Equal("yes", decision.IntentName);
Assert.Equal("Yes.", decision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_AlarmTimerNoneSetPrompt_MapsShortDenialToNoIntent()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "no",
NormalizedTranscript = "no",
Attributes = new Dictionary
{
["listenRules"] = (string[])["clock/alarm_timer_none_set", "globals/global_commands_launch"],
["listenAsrHints"] = (string[])["$YESNO"]
}
});
Assert.Equal("no", decision.IntentName);
Assert.Equal("No.", decision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_SharedYesNoPrompt_MapsAffirmativeWordToYesIntent()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "affirmative",
NormalizedTranscript = "affirmative",
Attributes = new Dictionary
{
["listenRules"] = (string[])["shared/yes_no", "globals/gui_nav"],
["listenAsrHints"] = (string[])["$YESNO"]
}
});
Assert.Equal("yes", decision.IntentName);
Assert.Equal("Yes.", decision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_SharedYesNoPrompt_MapsNegativeWordToNoIntent()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "negative",
NormalizedTranscript = "negative",
Attributes = new Dictionary
{
["listenRules"] = (string[])["shared/yes_no", "globals/gui_nav"],
["listenAsrHints"] = (string[])["$YESNO"]
}
});
Assert.Equal("no", decision.IntentName);
Assert.Equal("No.", decision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_SurprisesDateOfferPrompt_WithNoisyAffirmation_MapsToSurpriseIntent()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "- Thank you. - Yes.",
NormalizedTranscript = "- Thank you. - Yes.",
Attributes = new Dictionary
{
["listenRules"] = (string[])["surprises-date/offer_date_fact", "globals/gui_nav", "globals/global_commands_launch"],
["listenAsrHints"] = (string[])["$YESNO"],
["context"] = """{"runtime":{"location":{"iso":"2026-04-20T08:00:00-05:00"}}}"""
}
});
Assert.Equal("proactive_offer_pizza_fact", decision.IntentName);
Assert.Equal("Do you want to hear a fun pizza fact?", decision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_WordOfDayOfferPrompt_WithNoisyAffirmation_MapsToWordOfDayLaunch()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "- Me. - Yes.",
NormalizedTranscript = "- Me. - Yes.",
Attributes = new Dictionary
{
["listenRules"] = (string[])["word-of-the-day/surprise", "globals/gui_nav", "globals/global_commands_launch"],
["listenAsrHints"] = (string[])["$YESNO"]
}
});
Assert.Equal("word_of_the_day", decision.IntentName);
Assert.Equal("Starting word of the day.", decision.ReplyText);
Assert.Equal("@be/word-of-the-day", decision.SkillName);
}
[Fact]
public async Task BuildDecisionAsync_SettingsDownloadPrompt_MapsShortDenialToNoIntent()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "No.",
NormalizedTranscript = "No.",
Attributes = new Dictionary
{
["listenRules"] = (string[])["settings/download_now_later", "globals/global_commands_launch"]
}
});
Assert.Equal("no", decision.IntentName);
Assert.Equal("No.", decision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_SurprisesDateOfferPrompt_MapsShortAffirmationToSurpriseFlow()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "Yes!",
NormalizedTranscript = "Yes!",
Attributes = new Dictionary
{
["listenRules"] = (string[])["surprises-date/offer_date_fact", "globals/global_commands_launch"],
["listenAsrHints"] = (string[])["$YESNO"]
}
});
Assert.Equal("proactive_offer_pizza_fact", decision.IntentName);
Assert.Equal("Do you want to hear a fun pizza fact?", decision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_SkillPhraseVariant_MapsToKnownIntent()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "make me laugh",
NormalizedTranscript = "make me laugh"
});
Assert.Equal("joke", decision.IntentName);
}
[Fact]
public async Task BuildDecisionAsync_OpenTheRadio_MapsToRadioLaunchIntent()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "open the radio",
NormalizedTranscript = "open the radio"
});
Assert.Equal("radio", decision.IntentName);
Assert.Equal("@be/radio", decision.SkillName);
Assert.Equal("@be/radio", decision.SkillPayload!["skillId"]);
}
[Fact]
public async Task BuildDecisionAsync_PlayCountryMusic_MapsToRadioGenreLaunchIntent()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "play country music",
NormalizedTranscript = "play country music"
});
Assert.Equal("radio_genre", decision.IntentName);
Assert.Equal("@be/radio", decision.SkillName);
Assert.Equal("Country", decision.SkillPayload!["station"]);
}
[Fact]
public async Task BuildDecisionAsync_StopThat_MapsToIdleStopCommand()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "stop that",
NormalizedTranscript = "stop that"
});
Assert.Equal("stop", decision.IntentName);
Assert.Equal("@be/idle", decision.SkillName);
Assert.Equal("stop", decision.SkillPayload!["globalIntent"]);
Assert.Equal("global_commands", decision.SkillPayload["nluDomain"]);
}
[Fact]
public async Task BuildDecisionAsync_NeverMindWithPunctuation_MapsToIdleStopCommand()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "Never mind.",
NormalizedTranscript = "Never mind."
});
Assert.Equal("stop", decision.IntentName);
Assert.Equal("@be/idle", decision.SkillName);
}
[Fact]
public async Task BuildDecisionAsync_TurnItUp_MapsToGlobalVolumeUpCommand()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "turn it up",
NormalizedTranscript = "turn it up"
});
Assert.Equal("volume_up", decision.IntentName);
Assert.Equal("global_commands", decision.SkillName);
Assert.Equal("volumeUp", decision.SkillPayload!["globalIntent"]);
Assert.Equal("null", decision.SkillPayload["volumeLevel"]);
}
[Fact]
public async Task BuildDecisionAsync_SetVolumeToSix_MapsToGlobalVolumeToValueCommand()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "set volume to six",
NormalizedTranscript = "set volume to six"
});
Assert.Equal("volume_to_value", decision.IntentName);
Assert.Equal("global_commands", decision.SkillName);
Assert.Equal("volumeToValue", decision.SkillPayload!["globalIntent"]);
Assert.Equal("6", decision.SkillPayload["volumeLevel"]);
}
[Fact]
public async Task BuildDecisionAsync_SetVolumeTwoSix_UsesTrailingHomophoneLevel()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "Set Volume 2-6.",
NormalizedTranscript = "Set Volume 2-6."
});
Assert.Equal("volume_to_value", decision.IntentName);
Assert.Equal("volumeToValue", decision.SkillPayload!["globalIntent"]);
Assert.Equal("6", decision.SkillPayload["volumeLevel"]);
}
[Fact]
public async Task BuildDecisionAsync_ShowVolumeControls_MapsToSettingsVolumeQuery()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "show volume controls",
NormalizedTranscript = "show volume controls"
});
Assert.Equal("volume_query", decision.IntentName);
Assert.Equal("@be/settings", decision.SkillName);
Assert.Equal("volumeQuery", decision.SkillPayload!["localIntent"]);
}
[Fact]
public async Task BuildDecisionAsync_OpenPhotogal_MapsToGalleryLaunch()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "open the photogal",
NormalizedTranscript = "open the photogal"
});
Assert.Equal("photo_gallery", decision.IntentName);
Assert.Equal("@be/gallery", decision.SkillName);
}
[Fact]
public async Task BuildDecisionAsync_OpenTimer_MapsToLocalClockTimerMenu()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "open timer",
NormalizedTranscript = "open timer"
});
Assert.Equal("timer_menu", decision.IntentName);
Assert.Equal("@be/clock", decision.SkillName);
Assert.Equal("timer", decision.SkillPayload!["domain"]);
Assert.Equal("menu", decision.SkillPayload["clockIntent"]);
}
[Fact]
public async Task BuildDecisionAsync_OpenClock_MapsToDirectClockView()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "open the clock",
NormalizedTranscript = "open the clock"
});
Assert.Equal("clock_open", decision.IntentName);
Assert.Equal("@be/clock", decision.SkillName);
Assert.Equal("clock", decision.SkillPayload!["domain"]);
Assert.Equal("askForTime", decision.SkillPayload["clockIntent"]);
}
[Fact]
public async Task BuildDecisionAsync_WhatTimeIsIt_MapsToLocalClockTimeIntent()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "what time is it",
NormalizedTranscript = "what time is it"
});
Assert.Equal("time", decision.IntentName);
Assert.Equal("@be/clock", decision.SkillName);
Assert.Equal("askForTime", decision.SkillPayload!["clockIntent"]);
}
[Fact]
public async Task BuildDecisionAsync_TodaysDate_MapsToLocalClockDateIntent()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "what's today's date",
NormalizedTranscript = "what's today's date"
});
Assert.Equal("date", decision.IntentName);
Assert.Equal("@be/clock", decision.SkillName);
Assert.Equal("askForDate", decision.SkillPayload!["clockIntent"]);
}
[Fact]
public async Task BuildDecisionAsync_SetTimerForFiveMinutes_MapsToClockStartIntent()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "set a timer for five minutes",
NormalizedTranscript = "set a timer for five minutes"
});
Assert.Equal("timer_value", decision.IntentName);
Assert.Equal("@be/clock", decision.SkillName);
Assert.Equal("timer", decision.SkillPayload!["domain"]);
Assert.Equal("start", decision.SkillPayload["clockIntent"]);
Assert.Equal("0", decision.SkillPayload["hours"]);
Assert.Equal("5", decision.SkillPayload["minutes"]);
Assert.Equal("null", decision.SkillPayload["seconds"]);
}
[Fact]
public async Task BuildDecisionAsync_SetAlarmForSevenThirtyAm_MapsToClockStartIntent()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "set an alarm for 7:30 am",
NormalizedTranscript = "set an alarm for 7:30 am"
});
Assert.Equal("alarm_value", decision.IntentName);
Assert.Equal("@be/clock", decision.SkillName);
Assert.Equal("alarm", decision.SkillPayload!["domain"]);
Assert.Equal("start", decision.SkillPayload["clockIntent"]);
Assert.Equal("7:30", decision.SkillPayload["time"]);
Assert.Equal("am", decision.SkillPayload["ampm"]);
}
[Fact]
public async Task BuildDecisionAsync_SetAlarmForEightThirty_ParsesCompactTime()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "set an alarm for 830",
NormalizedTranscript = "set an alarm for 830"
});
Assert.Equal("alarm_value", decision.IntentName);
Assert.Equal("8:30", decision.SkillPayload!["time"]);
Assert.Equal("am", decision.SkillPayload["ampm"]);
}
[Fact]
public async Task BuildDecisionAsync_SetAlarmForEightThirtySpokenDigits_ParsesSplitTime()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "set an alarm for 8 30",
NormalizedTranscript = "set an alarm for 8 30"
});
Assert.Equal("alarm_value", decision.IntentName);
Assert.Equal("8:30", decision.SkillPayload!["time"]);
Assert.Equal("am", decision.SkillPayload["ampm"]);
}
[Fact]
public async Task BuildDecisionAsync_SetAlarmForTenTwentyFiveWithHyphen_ParsesSplitTime()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "set an alarm for 10-25",
NormalizedTranscript = "set an alarm for 10-25"
});
Assert.Equal("alarm_value", decision.IntentName);
Assert.Equal("10:25", decision.SkillPayload!["time"]);
Assert.Equal("am", decision.SkillPayload["ampm"]);
}
[Fact]
public async Task BuildDecisionAsync_SetAlarmForTenTwentyFivePm_ParsesPmSuffix()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "set an alarm for 10:25 pm",
NormalizedTranscript = "set an alarm for 10:25 pm"
});
Assert.Equal("alarm_value", decision.IntentName);
Assert.Equal("10:25", decision.SkillPayload!["time"]);
Assert.Equal("pm", decision.SkillPayload["ampm"]);
}
[Fact]
public async Task BuildDecisionAsync_SetAlarmForTenTwentyFiveSpacedPm_ParsesPmSuffix()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "set an alarm for 10 25 p m",
NormalizedTranscript = "set an alarm for 10 25 p m"
});
Assert.Equal("alarm_value", decision.IntentName);
Assert.Equal("10:25", decision.SkillPayload!["time"]);
Assert.Equal("pm", decision.SkillPayload["ampm"]);
}
[Fact]
public async Task BuildDecisionAsync_SetAlarmForSevenEighteen_UsesNextOccurrenceFromContext()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "set an alarm for 7:18",
NormalizedTranscript = "set an alarm for 7:18",
Attributes = new Dictionary
{
["context"] = """{"runtime":{"location":{"iso":"2026-04-22T07:15:00-05:00"}}}"""
}
});
Assert.Equal("alarm_value", decision.IntentName);
Assert.Equal("7:18", decision.SkillPayload!["time"]);
Assert.Equal("am", decision.SkillPayload["ampm"]);
}
[Fact]
public async Task BuildDecisionAsync_SetAlarmForSevenTen_UsesNextOccurrenceFromContext()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "set an alarm for 7:10",
NormalizedTranscript = "set an alarm for 7:10",
Attributes = new Dictionary
{
["context"] = """{"runtime":{"location":{"iso":"2026-04-22T07:15:00-05:00"}}}"""
}
});
Assert.Equal("alarm_value", decision.IntentName);
Assert.Equal("7:10", decision.SkillPayload!["time"]);
Assert.Equal("pm", decision.SkillPayload["ampm"]);
}
[Fact]
public async Task BuildDecisionAsync_TimerValueFollowUp_ParsesBareDuration()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "twenty five minutes",
NormalizedTranscript = "twenty five minutes",
Attributes = new Dictionary
{
["listenRules"] = (string[])["clock/timer_set_value"]
}
});
Assert.Equal("timer_value", decision.IntentName);
Assert.Equal("@be/clock", decision.SkillName);
Assert.Equal("start", decision.SkillPayload!["clockIntent"]);
Assert.Equal("25", decision.SkillPayload["minutes"]);
}
[Fact]
public async Task BuildDecisionAsync_AlarmValueFollowUp_ParsesBareSpokenTime()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "ten twenty five",
NormalizedTranscript = "ten twenty five",
Attributes = new Dictionary
{
["listenRules"] = (string[])["clock/alarm_set_value"]
}
});
Assert.Equal("alarm_value", decision.IntentName);
Assert.Equal("@be/clock", decision.SkillName);
Assert.Equal("start", decision.SkillPayload!["clockIntent"]);
Assert.Equal("10:25", decision.SkillPayload["time"]);
Assert.Equal("am", decision.SkillPayload["ampm"]);
}
[Fact]
public async Task BuildDecisionAsync_AlarmValueFollowUp_ParsesCommaSeparatedSpokenDigits()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "7, 44",
NormalizedTranscript = "7, 44",
Attributes = new Dictionary
{
["listenRules"] = (string[])["clock/alarm_set_value"],
["context"] = """{"runtime":{"location":{"iso":"2026-04-26T07:43:00-05:00"}}}"""
}
});
Assert.Equal("alarm_value", decision.IntentName);
Assert.Equal("@be/clock", decision.SkillName);
Assert.Equal("start", decision.SkillPayload!["clockIntent"]);
Assert.Equal("7:44", decision.SkillPayload["time"]);
Assert.Equal("am", decision.SkillPayload["ampm"]);
}
[Fact]
public async Task BuildDecisionAsync_SetAlarmWithoutTime_AsksForClarification()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "set an alarm",
NormalizedTranscript = "set an alarm"
});
Assert.Equal("alarm_clarify", decision.IntentName);
Assert.Equal("@be/clock", decision.SkillName);
Assert.Equal("alarm", decision.SkillPayload!["domain"]);
Assert.Equal("set", decision.SkillPayload["clockIntent"]);
Assert.Equal("What time should I set the alarm for?", decision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_CancelAlarm_MapsToClockDeleteIntent()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "cancel alarm",
NormalizedTranscript = "cancel alarm"
});
Assert.Equal("alarm_delete", decision.IntentName);
Assert.Equal("@be/clock", decision.SkillName);
Assert.Equal("alarm", decision.SkillPayload!["domain"]);
Assert.Equal("delete", decision.SkillPayload["clockIntent"]);
}
[Theory]
[InlineData("delete the alarm")]
[InlineData("so, delete the alarm")]
[InlineData("delete along")]
[InlineData("so, delete the along")]
public async Task BuildDecisionAsync_DeleteAlarmVariants_MapsToClockDeleteIntent(string transcript)
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = transcript,
NormalizedTranscript = transcript
});
Assert.Equal("alarm_delete", decision.IntentName);
Assert.Equal("@be/clock", decision.SkillName);
Assert.Equal("alarm", decision.SkillPayload!["domain"]);
Assert.Equal("delete", decision.SkillPayload["clockIntent"]);
}
[Fact]
public async Task BuildDecisionAsync_ClientNluSetAlarmWithoutTime_AsksForClarification()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "set",
NormalizedTranscript = "set",
Attributes = new Dictionary
{
["clientIntent"] = "set",
["clientEntities"] = new Dictionary
{
["domain"] = "alarm"
},
["clientRules"] = (string[])["clock/clock_menu"]
}
});
Assert.Equal("alarm_clarify", decision.IntentName);
Assert.Equal("@be/clock", decision.SkillName);
Assert.Equal("alarm", decision.SkillPayload!["domain"]);
Assert.Equal("set", decision.SkillPayload["clockIntent"]);
}
[Fact]
public async Task BuildDecisionAsync_ClientNluCancelFromAlarmQueryMenu_UsesLastClockDomain()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "cancel",
NormalizedTranscript = "cancel",
Attributes = new Dictionary
{
["clientIntent"] = "cancel",
["clientRules"] = (string[])["clock/alarm_timer_query_menu"],
["lastClockDomain"] = "alarm"
}
});
Assert.Equal("alarm_delete", decision.IntentName);
Assert.Equal("@be/clock", decision.SkillName);
Assert.Equal("alarm", decision.SkillPayload!["domain"]);
Assert.Equal("delete", decision.SkillPayload["clockIntent"]);
}
[Fact]
public async Task BuildDecisionAsync_ClientNluCancelFromAlarmValuePrompt_MapsToClockCancelIntent()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "cancel",
NormalizedTranscript = "cancel",
Attributes = new Dictionary
{
["clientIntent"] = "cancel",
["clientRules"] = (string[])["clock/alarm_set_value"]
}
});
Assert.Equal("alarm_cancel", decision.IntentName);
Assert.Equal("@be/clock", decision.SkillName);
Assert.Equal("alarm", decision.SkillPayload!["domain"]);
Assert.Equal("cancel", decision.SkillPayload["clockIntent"]);
}
[Fact]
public async Task BuildDecisionAsync_SetTimerWithoutDuration_AsksForClarification()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "set a timer",
NormalizedTranscript = "set a timer"
});
Assert.Equal("timer_clarify", decision.IntentName);
Assert.Equal("@be/clock", decision.SkillName);
Assert.Equal("timer", decision.SkillPayload!["domain"]);
Assert.Equal("set", decision.SkillPayload["clockIntent"]);
Assert.Equal("How long should I set the timer for?", decision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_OpenPhotoGallery_MapsToGalleryLaunch()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "open photo gallery",
NormalizedTranscript = "open photo gallery"
});
Assert.Equal("photo_gallery", decision.IntentName);
Assert.Equal("@be/gallery", decision.SkillName);
Assert.Equal("menu", decision.SkillPayload!["localIntent"]);
}
[Fact]
public async Task BuildDecisionAsync_SnapAPicture_MapsToCreateOnePhoto()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "snap a picture",
NormalizedTranscript = "snap a picture"
});
Assert.Equal("snapshot", decision.IntentName);
Assert.Equal("@be/create", decision.SkillName);
Assert.Equal("createOnePhoto", decision.SkillPayload!["localIntent"]);
}
[Fact]
public async Task BuildDecisionAsync_OpenPhotobooth_MapsToCreateSomePhotos()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "open photobooth",
NormalizedTranscript = "open photobooth"
});
Assert.Equal("photobooth", decision.IntentName);
Assert.Equal("@be/create", decision.SkillName);
Assert.Equal("createSomePhotos", decision.SkillPayload!["localIntent"]);
}
[Fact]
public async Task BuildDecisionAsync_TellMeTheNews_UsesNimbusCloudSkillPath()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "tell me the news",
NormalizedTranscript = "tell me the news"
});
Assert.Equal("news", decision.IntentName);
Assert.Equal("news", decision.SkillName);
Assert.Equal("news", decision.SkillPayload!["skillId"]);
Assert.Equal("news", decision.SkillPayload["cloudSkill"]);
Assert.Equal("runtime-news", decision.SkillPayload["mim_id"]);
Assert.Equal("provider_unavailable", decision.SkillPayload["news_provider_status"]);
Assert.DoesNotContain("future cloud integration", decision.ReplyText, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public async Task BuildDecisionAsync_TellMeTheNews_WithProvider_UsesProviderHeadlines()
{
var provider = new CapturingNewsBriefingProvider
{
Snapshot = new NewsBriefingSnapshot(
[
new NewsHeadline("Local robotics team unveils weather-ready helper"),
new NewsHeadline("Community makerspace hosts weekend AI expo")
],
"NewsAPI")
};
var service = CreateService(newsBriefingProvider: provider);
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "tell me the news",
NormalizedTranscript = "tell me the news"
});
Assert.Equal("news", decision.IntentName);
Assert.Equal("news", decision.SkillName);
Assert.Equal("news", decision.SkillPayload!["skillId"]);
Assert.Equal("news", decision.SkillPayload["cloudSkill"]);
Assert.Equal("runtime-news", decision.SkillPayload["mim_id"]);
Assert.Contains("news-stinger", decision.SkillPayload["esml"]?.ToString(), StringComparison.OrdinalIgnoreCase);
Assert.Equal("NewsAPI", decision.SkillPayload["news_source"]);
Assert.Equal(2, decision.SkillPayload["news_headline_count"]);
Assert.Equal("provider_success", decision.SkillPayload["news_provider_status"]);
Assert.Equal(3, decision.SkillPayload["news_provider_requested_headlines"]);
Assert.Equal(2, decision.SkillPayload["news_provider_resolved_headlines"]);
Assert.Contains("Local robotics team unveils weather-ready helper", decision.ReplyText, StringComparison.OrdinalIgnoreCase);
Assert.NotNull(provider.LastRequest);
Assert.Equal(3, provider.LastRequest!.MaxHeadlines);
}
[Fact]
public async Task BuildDecisionAsync_TellMeTheNews_WithAIAlias_UsesTechnologyCategory()
{
var provider = new CapturingNewsBriefingProvider
{
Snapshot = new NewsBriefingSnapshot(
[
new NewsHeadline("AI labs unveil new home companion behaviors")
],
"NewsAPI")
};
var service = CreateService(newsBriefingProvider: provider);
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "tell me the a i news",
NormalizedTranscript = "tell me the a i news"
});
Assert.Equal("news", decision.IntentName);
Assert.NotNull(provider.LastRequest);
Assert.Contains("technology", provider.LastRequest!.PreferredCategories, StringComparer.OrdinalIgnoreCase);
Assert.Contains("artificial intelligence", decision.SkillPayload?["esml"]?.ToString(), StringComparison.OrdinalIgnoreCase);
}
[Fact]
public async Task BuildDecisionAsync_TellMeTheNews_WithMemoryPreference_UsesCategoryHints()
{
var memoryStore = new InMemoryPersonalMemoryStore();
var provider = new CapturingNewsBriefingProvider
{
Snapshot = new NewsBriefingSnapshot(
[
new NewsHeadline("City soccer clubs prepare for summer playoffs")
],
"NewsAPI")
};
var service = CreateService(memoryStore, newsBriefingProvider: provider);
await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "i like sports",
NormalizedTranscript = "i like sports",
Attributes = new Dictionary
{
["accountId"] = "acct-a",
["loopId"] = "loop-a"
},
DeviceId = "device-a"
});
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "tell me the news",
NormalizedTranscript = "tell me the news",
Attributes = new Dictionary
{
["accountId"] = "acct-a",
["loopId"] = "loop-a"
},
DeviceId = "device-a"
});
Assert.Equal("news", decision.IntentName);
Assert.NotNull(provider.LastRequest);
Assert.Contains("sports", provider.LastRequest!.PreferredCategories, StringComparer.OrdinalIgnoreCase);
}
[Fact]
public async Task BuildDecisionAsync_CloudVersion_UsesSharedBuildInfo()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "what's the cloud version",
NormalizedTranscript = "what's the cloud version"
});
Assert.Equal("cloud_version", decision.IntentName);
Assert.Equal(OpenJiboCloudBuildInfo.SpokenVersion, decision.ReplyText);
Assert.DoesNotContain("Jibo", decision.ReplyText, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public async Task BuildDecisionAsync_WordOfDayGuess_UsesStructuredClientNluGuess()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "guess",
NormalizedTranscript = "guess",
Attributes = new Dictionary
{
["clientIntent"] = "guess",
["clientRules"] = (string[])["word-of-the-day/puzzle"],
["clientEntities"] = JsonDocument.Parse("""{"guess":"pastoral"}""").RootElement.Clone()
}
});
Assert.Equal("word_of_the_day_guess", decision.IntentName);
Assert.Equal("I heard pastoral.", decision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_WordOfDayGuess_UsesSpokenTranscriptDuringPuzzleTurn()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "pastoral",
NormalizedTranscript = "pastoral",
Attributes = new Dictionary
{
["listenRules"] = (string[])["word-of-the-day/puzzle"]
}
});
Assert.Equal("word_of_the_day_guess", decision.IntentName);
Assert.Equal("I heard pastoral.", decision.ReplyText);
}
[Fact]
public async Task BuildDecisionAsync_WordOfDayStartPhrase_MapsToSkillIntent()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "start word of the day",
NormalizedTranscript = "start word of the day"
});
Assert.Equal("word_of_the_day", decision.IntentName);
Assert.Equal("Starting word of the day.", decision.ReplyText);
Assert.Equal("@be/word-of-the-day", decision.SkillName);
Assert.Equal("word-of-the-day", decision.SkillPayload!["domain"]);
Assert.Equal("@be/word-of-the-day", decision.SkillPayload["skillId"]);
}
[Fact]
public async Task BuildDecisionAsync_WordOfDayGuess_LineNumberUsesListenHints()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "Two.",
NormalizedTranscript = "Two.",
Attributes = new Dictionary
{
["listenRules"] = (string[])["word-of-the-day/puzzle"],
["listenAsrHints"] = (string[])["doodad", "pastoral", "escarpment"]
}
});
Assert.Equal("word_of_the_day_guess", decision.IntentName);
Assert.Equal("I heard pastoral.", decision.ReplyText);
Assert.Equal("@be/word-of-the-day", decision.SkillName);
Assert.Equal("pastoral", decision.SkillPayload!["guess"]);
Assert.Equal("@be/word-of-the-day", decision.SkillPayload["skillId"]);
Assert.Equal("completion_only", decision.SkillPayload["cloudResponseMode"]);
}
[Fact]
public async Task BuildDecisionAsync_WordOfDayGuess_FuzzyMatchesClosestHint()
{
var service = CreateService();
var decision = await service.BuildDecisionAsync(new TurnContext
{
RawTranscript = "Haglet.",
NormalizedTranscript = "Haglet.",
Attributes = new Dictionary
{
["listenRules"] = (string[])["word-of-the-day/puzzle"],
["listenAsrHints"] = (string[])["aglet", "hovel", "wisenheimer"]
}
});
Assert.Equal("word_of_the_day_guess", decision.IntentName);
Assert.Equal("I heard aglet.", decision.ReplyText);
Assert.Equal("aglet", decision.SkillPayload!["guess"]);
}
private static JiboInteractionService CreateService(
IPersonalMemoryStore? personalMemoryStore = null,
IWeatherReportProvider? weatherReportProvider = null,
INewsBriefingProvider? newsBriefingProvider = null,
IJiboExperienceContentRepository? contentRepository = null)
{
return new JiboInteractionService(
new JiboExperienceContentCache(contentRepository ?? new InMemoryJiboExperienceContentRepository()),
new FirstItemRandomizer(),
personalMemoryStore ?? new InMemoryPersonalMemoryStore(),
weatherReportProvider,
newsBriefingProvider);
}
private sealed class FirstItemRandomizer : IJiboRandomizer
{
public T Choose(IReadOnlyList items)
{
return items[0];
}
}
private sealed class CapturingWeatherReportProvider : IWeatherReportProvider
{
public WeatherReportRequest? LastRequest { get; private set; }
public List Requests { get; } = [];
public WeatherReportSnapshot? Snapshot { get; init; }
public Task GetReportAsync(
WeatherReportRequest request,
CancellationToken cancellationToken = default)
{
LastRequest = request;
Requests.Add(request);
return Task.FromResult(Snapshot);
}
}
private sealed class CapturingNewsBriefingProvider : INewsBriefingProvider
{
public NewsBriefingRequest? LastRequest { get; private set; }
public NewsBriefingSnapshot? Snapshot { get; init; }
public Task GetBriefingAsync(
NewsBriefingRequest request,
CancellationToken cancellationToken = default)
{
LastRequest = request;
return Task.FromResult(Snapshot);
}
}
private sealed class StaticCatalogRepository(JiboExperienceCatalog catalog) : IJiboExperienceContentRepository
{
public Task GetCatalogAsync(CancellationToken cancellationToken = default)
{
return Task.FromResult(catalog);
}
}
}